@reventlessdev/reventless-core 3.0.0-alpha.211 → 3.0.0-alpha.213

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 (32) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +7 -7
  3. package/src/Projection.res +5 -2
  4. package/src/Projection.res.mjs +2 -1
  5. package/src/admin/Platform_AdminApi.res +2 -0
  6. package/src/admin/Platform_AdminApi.res.mjs +3 -1
  7. package/src/admin/Platform_ComponentDefinitionsApi.res +26 -5
  8. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +21 -5
  9. package/src/admin/Platform_PluginStructuresApi.res +132 -0
  10. package/src/admin/Platform_PluginStructuresApi.res.mjs +117 -0
  11. package/src/components/AutomationSlice/AutomationSlice_Callback.res +30 -0
  12. package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +12 -0
  13. package/src/components/Dcb/Dcb_Builder.res +6 -2
  14. package/src/components/Dcb/Dcb_Builder.res.mjs +2 -2
  15. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res +32 -0
  16. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs +13 -0
  17. package/src/plugin/component/Plugin_Builder.res +6 -13
  18. package/src/plugin/component/Plugin_Builder.res.mjs +2 -16
  19. package/src/plugin/component/Plugin_Helpers.res +21 -3
  20. package/src/plugin/component/Plugin_Helpers.res.mjs +16 -0
  21. package/src/util/LogFormat.res +52 -1
  22. package/src/util/LogFormat.res.mjs +21 -2
  23. package/src/util/StoreLayout.res +38 -0
  24. package/src/util/StoreLayout.res.mjs +22 -0
  25. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +12 -7
  26. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +2 -2
  27. package/tests/admin/Platform_PluginStructuresApiTest.res +144 -0
  28. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +197 -0
  29. package/tests/api/GraphQL_FragmentGeneratorTest.res +40 -0
  30. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +35 -0
  31. package/tests/logger/LogFormatTest.res +42 -0
  32. package/tests/logger/LogFormatTest.res.mjs +37 -0
@@ -0,0 +1,197 @@
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 Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
7
+ import * as Platform_PluginStructuresApi$ReventlessCore from "../../src/admin/Platform_PluginStructuresApi.res.mjs";
8
+
9
+ let publicRm_consumedEventTypes = ["ProductAdded"];
10
+
11
+ let publicRm_linkedWriteSide = ["Product"];
12
+
13
+ let publicRm_searchableFields = ["displayName"];
14
+
15
+ let publicRm_labelFieldSource = "annotation";
16
+
17
+ let publicRm_chapter = "Inventory";
18
+
19
+ let publicRm = {
20
+ name: "Products",
21
+ queryField: "Catalog_Products",
22
+ schema: "{}",
23
+ consumedEventTypes: publicRm_consumedEventTypes,
24
+ linkedWriteSide: publicRm_linkedWriteSide,
25
+ labelField: "displayName",
26
+ searchableFields: publicRm_searchableFields,
27
+ labelFieldSource: publicRm_labelFieldSource,
28
+ statusField: undefined,
29
+ visibility: undefined,
30
+ chapter: publicRm_chapter
31
+ };
32
+
33
+ let internalRm_consumedEventTypes = publicRm_consumedEventTypes;
34
+
35
+ let internalRm_linkedWriteSide = publicRm_linkedWriteSide;
36
+
37
+ let internalRm_searchableFields = publicRm_searchableFields;
38
+
39
+ let internalRm_labelFieldSource = "annotation";
40
+
41
+ let internalRm_visibility = "Internal";
42
+
43
+ let internalRm_chapter = "Inventory";
44
+
45
+ let internalRm = {
46
+ name: "ProductAudit",
47
+ queryField: "Catalog_ProductAudit",
48
+ schema: "{}",
49
+ consumedEventTypes: internalRm_consumedEventTypes,
50
+ linkedWriteSide: internalRm_linkedWriteSide,
51
+ labelField: "displayName",
52
+ searchableFields: internalRm_searchableFields,
53
+ labelFieldSource: internalRm_labelFieldSource,
54
+ statusField: undefined,
55
+ visibility: internalRm_visibility,
56
+ chapter: internalRm_chapter
57
+ };
58
+
59
+ let structure_readModels = [
60
+ publicRm,
61
+ internalRm
62
+ ];
63
+
64
+ let structure_stateViewSlices = [internalRm];
65
+
66
+ let structure_stateChangeSlices = [];
67
+
68
+ let structure_aggregates = [];
69
+
70
+ let structure_automationSlices = [];
71
+
72
+ let structure_outboundTranslationSlices = [];
73
+
74
+ let structure_inboundTranslationSlices = [];
75
+
76
+ let structure_extensions = [];
77
+
78
+ let structure_extensionPoints = [{
79
+ name: "Catalog.Products",
80
+ delegateNames: ["onAdded"],
81
+ sourceEventTypes: ["Catalog.ProductAdded"],
82
+ commandTypes: undefined
83
+ }];
84
+
85
+ let structure_requiredStores = ["Catalog.images"];
86
+
87
+ let structure_requiredStoreDeclarations = [{
88
+ store: "Catalog.images",
89
+ component: "Product",
90
+ field: "image",
91
+ annotation: "images"
92
+ }];
93
+
94
+ let structure = {
95
+ readModels: structure_readModels,
96
+ stateViewSlices: structure_stateViewSlices,
97
+ stateChangeSlices: structure_stateChangeSlices,
98
+ aggregates: structure_aggregates,
99
+ automationSlices: structure_automationSlices,
100
+ outboundTranslationSlices: structure_outboundTranslationSlices,
101
+ inboundTranslationSlices: structure_inboundTranslationSlices,
102
+ extensions: structure_extensions,
103
+ extensionPoints: structure_extensionPoints,
104
+ requiredStores: structure_requiredStores,
105
+ requiredStoreDeclarations: structure_requiredStoreDeclarations
106
+ };
107
+
108
+ let encoded = Platform_PluginStructuresApi$ReventlessCore.encodePluginStructureEntry("Catalog", structure);
109
+
110
+ let json = JSON.stringify(encoded);
111
+
112
+ function namesOf(key) {
113
+ return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(encoded), d => d[key]), Stdlib_JSON.Decode.array), []), v => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(v), o => o["name"]), Stdlib_JSON.Decode.string));
114
+ }
115
+
116
+ globalThis.describe("Platform_PluginStructures entry", () => {
117
+ globalThis.test("carries the structure-level keys the AutoUI entry omits", () => {
118
+ let dict = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(encoded), {});
119
+ let keys = Object.keys(dict).toSorted(Primitive_string.compare);
120
+ globalThis.expect(keys).toEqual([
121
+ "aggregates",
122
+ "automationSlices",
123
+ "extensionPoints",
124
+ "extensions",
125
+ "inboundTranslationSlices",
126
+ "outboundTranslationSlices",
127
+ "pluginId",
128
+ "readModels",
129
+ "requiredStoreDeclarations",
130
+ "requiredStores",
131
+ "stateChangeSlices",
132
+ "stateViewSlices"
133
+ ]);
134
+ });
135
+ globalThis.test("keeps Internal read models", () => {
136
+ globalThis.expect(namesOf("readModels")).toEqual([
137
+ "Products",
138
+ "ProductAudit"
139
+ ]);
140
+ });
141
+ globalThis.test("keeps Internal state-view slices", () => {
142
+ globalThis.expect(namesOf("stateViewSlices")).toEqual(["ProductAudit"]);
143
+ });
144
+ globalThis.test("carries visibility so a consumer can tell which are Internal", () => {
145
+ globalThis.expect(json.includes("\"visibility\":\"Internal\"")).toEqual(true);
146
+ });
147
+ globalThis.test("encodes the chapter band", () => {
148
+ globalThis.expect(json.includes("\"chapter\":\"Inventory\"")).toEqual(true);
149
+ });
150
+ globalThis.test("encodes extension points with their source event types", () => {
151
+ globalThis.expect(json.includes("\"extensionPoints\":[{\"name\":\"Catalog.Products\",\"delegateNames\":[\"onAdded\"],\"sourceEventTypes\":[\"Catalog.ProductAdded\"],\"commandTypes\":null}]")).toEqual(true);
152
+ });
153
+ globalThis.test("encodes required stores with their provenance", () => {
154
+ globalThis.expect(json.includes("\"requiredStores\":[\"Catalog.images\"]")).toEqual(true);
155
+ });
156
+ });
157
+
158
+ globalThis.describe("absent optional collections", () => {
159
+ let bare_readModels = structure_readModels;
160
+ let bare_stateViewSlices = structure_stateViewSlices;
161
+ let bare_stateChangeSlices = structure_stateChangeSlices;
162
+ let bare_aggregates = structure_aggregates;
163
+ let bare_automationSlices = structure_automationSlices;
164
+ let bare_outboundTranslationSlices = structure_outboundTranslationSlices;
165
+ let bare_inboundTranslationSlices = structure_inboundTranslationSlices;
166
+ let bare_extensions = structure_extensions;
167
+ let bare = {
168
+ readModels: bare_readModels,
169
+ stateViewSlices: bare_stateViewSlices,
170
+ stateChangeSlices: bare_stateChangeSlices,
171
+ aggregates: bare_aggregates,
172
+ automationSlices: bare_automationSlices,
173
+ outboundTranslationSlices: bare_outboundTranslationSlices,
174
+ inboundTranslationSlices: bare_inboundTranslationSlices,
175
+ extensions: bare_extensions,
176
+ extensionPoints: undefined,
177
+ requiredStores: undefined,
178
+ requiredStoreDeclarations: undefined
179
+ };
180
+ let bareJson = JSON.stringify(Platform_PluginStructuresApi$ReventlessCore.encodePluginStructureEntry("Catalog", bare));
181
+ globalThis.test("encodes absent extensionPoints as null", () => {
182
+ globalThis.expect(bareJson.includes("\"extensionPoints\":null")).toEqual(true);
183
+ });
184
+ globalThis.test("encodes absent requiredStores as null", () => {
185
+ globalThis.expect(bareJson.includes("\"requiredStores\":null")).toEqual(true);
186
+ });
187
+ });
188
+
189
+ export {
190
+ publicRm,
191
+ internalRm,
192
+ structure,
193
+ encoded,
194
+ json,
195
+ namesOf,
196
+ }
197
+ /* encoded Not a pure module */
@@ -47,3 +47,43 @@ describe("GraphQL_FragmentGenerator.generate mutation id injection", () => {
47
47
  expect(field->String.includes("id: ID!, orderId"))->toBe(true)
48
48
  })
49
49
  })
50
+
51
+ // A slice TODO row stores its work item as `JSON.t`, which the IR can only call
52
+ // `Unknown` — and `Unknown` renders as `String!`. Serving a stored object
53
+ // through a String field fails at execution ("String cannot represent value:
54
+ // { … }") as soon as a client selects it, so `Dcb_Builder` describes the row
55
+ // with `todoRowSchemaFor` and the item's own schema instead.
56
+ module GeocodeItem = {
57
+ @schema
58
+ type outboundItem = {customerId: string, address: string}
59
+ }
60
+
61
+ let typeDefFor = (fragment, needle) =>
62
+ GraphQL_Stitcher.decode(fragment).types->Array.find(t => t->String.includes(needle))
63
+
64
+ describe("GraphQL_FragmentGenerator slice TODO rows", () => {
65
+ testSync("item carries the slice's item type, not String", () => {
66
+ let fragment = GraphQL_FragmentGenerator.generate(
67
+ ~mutationEntries=[],
68
+ ~queryEntries=[
69
+ {
70
+ ReventlessInfra.Api.singleFieldName: "Ordering_GeocodeCustomerAddressTodo",
71
+ listFieldName: "Ordering_GeocodeCustomerAddressTodos",
72
+ returnTypeName: "Ordering_GeocodeCustomerAddressTodo",
73
+ stateSchema: OutboundTranslationSlice_Callback.todoRowSchemaFor(
74
+ GeocodeItem.outboundItemSchema,
75
+ )->S.castToUnknown,
76
+ authorization: None,
77
+ connectionSpec: true,
78
+ },
79
+ ],
80
+ )
81
+ let row = typeDefFor(fragment, "type Ordering_GeocodeCustomerAddressTodo ")->Option.getOr("")
82
+ expect(row->String.includes("item: Ordering_GeocodeCustomerAddressTodoItem!"))->toBe(true)
83
+
84
+ let item =
85
+ typeDefFor(fragment, "type Ordering_GeocodeCustomerAddressTodoItem ")->Option.getOr("")
86
+ expect(item->String.includes("customerId: ID!"))->toBe(true)
87
+ expect(item->String.includes("address: String!"))->toBe(true)
88
+ })
89
+ })
@@ -4,6 +4,7 @@ import * as S from "sury/src/S.res.mjs";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as GraphQL_Stitcher$ReventlessCore from "../../src/components/Api/GraphQL_Stitcher.res.mjs";
6
6
  import * as GraphQL_FragmentGenerator$ReventlessCore from "../../src/components/Api/GraphQL_FragmentGenerator.res.mjs";
7
+ import * as OutboundTranslationSlice_Callback$ReventlessCore from "../../src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
7
8
 
8
9
  let commandSchema = S.union([
9
10
  S.schema(s => ({
@@ -50,8 +51,42 @@ globalThis.describe("GraphQL_FragmentGenerator.generate mutation id injection",
50
51
  });
51
52
  });
52
53
 
54
+ let outboundItemSchema = S.schema(s => ({
55
+ customerId: s.m(S.string),
56
+ address: s.m(S.string)
57
+ }));
58
+
59
+ let GeocodeItem = {
60
+ outboundItemSchema: outboundItemSchema,
61
+ externalSystem: undefined
62
+ };
63
+
64
+ function typeDefFor(fragment, needle) {
65
+ return GraphQL_Stitcher$ReventlessCore.decode(fragment).types.find(t => t.includes(needle));
66
+ }
67
+
68
+ globalThis.describe("GraphQL_FragmentGenerator slice TODO rows", () => {
69
+ globalThis.test("item carries the slice's item type, not String", () => {
70
+ let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([], [{
71
+ singleFieldName: "Ordering_GeocodeCustomerAddressTodo",
72
+ listFieldName: "Ordering_GeocodeCustomerAddressTodos",
73
+ returnTypeName: "Ordering_GeocodeCustomerAddressTodo",
74
+ stateSchema: OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchemaFor(outboundItemSchema),
75
+ authorization: undefined,
76
+ connectionSpec: true
77
+ }]);
78
+ let row = Stdlib_Option.getOr(typeDefFor(fragment, "type Ordering_GeocodeCustomerAddressTodo "), "");
79
+ globalThis.expect(row.includes("item: Ordering_GeocodeCustomerAddressTodoItem!")).toBe(true);
80
+ let item = Stdlib_Option.getOr(typeDefFor(fragment, "type Ordering_GeocodeCustomerAddressTodoItem "), "");
81
+ globalThis.expect(item.includes("customerId: ID!")).toBe(true);
82
+ globalThis.expect(item.includes("address: String!")).toBe(true);
83
+ });
84
+ });
85
+
53
86
  export {
54
87
  OrderSliceCmd,
55
88
  mutationFor,
89
+ GeocodeItem,
90
+ typeDefFor,
56
91
  }
57
92
  /* commandSchema Not a pure module */
@@ -127,6 +127,48 @@ describe("LogFormat", () => {
127
127
  },
128
128
  )
129
129
  })
130
+
131
+ // A ReScript record's absent optional field survives encoding as an own
132
+ // property whose value is `undefined` — the one thing `JSON.t` cannot hold and
133
+ // `JSON.stringify` answers with `undefined` instead of a string. Rendering a
134
+ // command carrying one must not reach a string function with that `undefined`:
135
+ // `variantFields` runs inside the CommandGenerator's log tap, so a throw there
136
+ // takes the command down with it and the slice never sees it.
137
+ describe("variantFields", () => {
138
+ module OptionalFieldCmd = {
139
+ @schema
140
+ type command = AddProduct({productId: string, name: string, imageUrl?: string})
141
+ }
142
+
143
+ testSync("omits an absent optional field", () => {
144
+ let json =
145
+ OptionalFieldCmd.AddProduct({productId: "prd-1", name: "Cirrus"})
146
+ ->Message.encode(OptionalFieldCmd.commandSchema)
147
+ expect(variantFields(json))->toEqual(`, {productId:"prd-1",name:"Cirrus"}`)
148
+ })
149
+
150
+ testSync("keeps a present optional field", () => {
151
+ let json =
152
+ OptionalFieldCmd.AddProduct({productId: "prd-1", name: "Cirrus", imageUrl: "/u/a.png"})
153
+ ->Message.encode(OptionalFieldCmd.commandSchema)
154
+ expect(variantFields(json))->toEqual(
155
+ `, {productId:"prd-1",name:"Cirrus",imageUrl:"/u/a.png"}`,
156
+ )
157
+ })
158
+
159
+ testSync("elides a value past the budget rather than dropping it", () => {
160
+ let long = String.repeat("x", maxValueChars + 20)
161
+ let json =
162
+ OptionalFieldCmd.AddProduct({productId: "prd-1", name: long})
163
+ ->Message.encode(OptionalFieldCmd.commandSchema)
164
+ let rendered = variantFields(json)
165
+ expect(rendered->String.includes("…(+22 chars)"))->toBe(true)
166
+ // The oversized value is capped, not the line: the neighbouring field that
167
+ // says which product this is stays intact.
168
+ expect(rendered->String.includes(long))->toBe(false)
169
+ expect(rendered->String.includes(`productId:"prd-1"`))->toBe(true)
170
+ })
171
+ })
130
172
  })
131
173
 
132
174
  // ─── JSON sink (non-TTY collector: CloudWatch / Datadog / …) ─────────────────
@@ -124,6 +124,43 @@ globalThis.describe("LogFormat", () => {
124
124
  globalThis.expect(msg).toEqual(`\x1b[1mVersionDetected\x1b[0m(testId): {"event":{"TAG":"VersionDetected","_0":"0.0.0"},"meta":{"service":"testService","time":"testTime","ip":"testIp","user":"testUser","msgId":"testMsgId","correlationId":"testCorrelationId"},"id":"testId"}`);
125
125
  });
126
126
  });
127
+ globalThis.describe("variantFields", () => {
128
+ let commandSchema = S.schema(s => ({
129
+ TAG: "AddProduct",
130
+ productId: s.m(S.string),
131
+ name: s.m(S.string),
132
+ imageUrl: s.m(S.option(S.string))
133
+ }));
134
+ globalThis.test("omits an absent optional field", () => {
135
+ let json = Message$ReventlessCore.encode({
136
+ TAG: "AddProduct",
137
+ productId: "prd-1",
138
+ name: "Cirrus"
139
+ }, commandSchema);
140
+ globalThis.expect(LogFormat$ReventlessCore.variantFields(json)).toEqual(`, {productId:"prd-1",name:"Cirrus"}`);
141
+ });
142
+ globalThis.test("keeps a present optional field", () => {
143
+ let json = Message$ReventlessCore.encode({
144
+ TAG: "AddProduct",
145
+ productId: "prd-1",
146
+ name: "Cirrus",
147
+ imageUrl: "/u/a.png"
148
+ }, commandSchema);
149
+ globalThis.expect(LogFormat$ReventlessCore.variantFields(json)).toEqual(`, {productId:"prd-1",name:"Cirrus",imageUrl:"/u/a.png"}`);
150
+ });
151
+ globalThis.test("elides a value past the budget rather than dropping it", () => {
152
+ let long = "x".repeat(LogFormat$ReventlessCore.maxValueChars + 20 | 0);
153
+ let json = Message$ReventlessCore.encode({
154
+ TAG: "AddProduct",
155
+ productId: "prd-1",
156
+ name: long
157
+ }, commandSchema);
158
+ let rendered = LogFormat$ReventlessCore.variantFields(json);
159
+ globalThis.expect(rendered.includes("…(+22 chars)")).toBe(true);
160
+ globalThis.expect(rendered.includes(long)).toBe(false);
161
+ globalThis.expect(rendered.includes(`productId:"prd-1"`)).toBe(true);
162
+ });
163
+ });
127
164
  });
128
165
 
129
166
  function hasAnsi(s) {