@reventlessdev/reventless-core 3.0.0-alpha.215 → 3.0.0-alpha.217

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 (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +6 -6
  3. package/src/ResourceAttribution.res +60 -4
  4. package/src/ResourceAttribution.res.mjs +26 -0
  5. package/src/adapter/QueryInterception/QueryInterception.res +74 -0
  6. package/src/adapter/QueryInterception/QueryInterception.res.mjs +26 -0
  7. package/src/adapter/ResourceAttribution_Deploytime.res +43 -0
  8. package/src/adapter/ResourceAttribution_Deploytime.res.mjs +20 -0
  9. package/src/admin/Platform_Admin_Structure.res +4 -0
  10. package/src/admin/Platform_Admin_Structure.res.mjs +5 -0
  11. package/src/admin/Platform_ComponentDefinitionsApi.res +16 -1
  12. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +24 -1
  13. package/src/components/Aggregate/Aggregate_Callback.res +14 -2
  14. package/src/components/Aggregate/Aggregate_Callback.res.mjs +3 -3
  15. package/src/components/Builder_Helpers.res +21 -3
  16. package/src/components/Builder_Helpers.res.mjs +4 -3
  17. package/src/components/CommandTopic/CommandTopic.res.mjs +12 -0
  18. package/src/components/CommandTopic/CommandTopic_Helpers.res +88 -2
  19. package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +48 -2
  20. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +4 -1
  21. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res.mjs +2 -1
  22. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +13 -1
  23. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
  24. package/src/plugin/component/Plugin_Builder.res +6 -1
  25. package/src/plugin/component/Plugin_Builder.res.mjs +4 -2
  26. package/src/plugin/component/Plugin_Helpers.res +7 -2
  27. package/src/plugin/component/Plugin_Helpers.res.mjs +4 -3
  28. package/src/plugin/component/Plugin_Structure.res +66 -51
  29. package/src/plugin/component/Plugin_Structure.res.mjs +61 -46
  30. package/tests/ResourceAttributionTest.res +115 -0
  31. package/tests/ResourceAttributionTest.res.mjs +106 -0
  32. package/tests/adapter/QueryInterceptionTest.res +55 -0
  33. package/tests/adapter/QueryInterceptionTest.res.mjs +49 -0
  34. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +21 -0
  35. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -0
  36. package/tests/commandtopic/CommandOutcomeHookTest.res +258 -0
  37. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +372 -0
  38. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +9 -1
  39. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +3 -3
  40. package/tests/message/MessageTest.res +1 -0
  41. package/tests/message/MessageTest.res.mjs +1 -0
  42. package/tests/plugin/PluginStructureTest.res +27 -0
  43. package/tests/plugin/PluginStructureTest.res.mjs +27 -0
  44. package/tests/plugin/StateChangeSlice/PsShipOrder.res +3 -1
  45. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -1
  46. package/tests/plugin/pluginDefinitionRequiredScalars.txt +8 -0
@@ -140,6 +140,62 @@ function labelFieldsFromStateSchema(entityName, stateSchema) {
140
140
  };
141
141
  }
142
142
 
143
+ function toEventDef(v) {
144
+ let mkDef = (variantName, properties) => {
145
+ let references = Stdlib_Array.filterMap(Object.entries(properties), param => {
146
+ let fieldName = param[0];
147
+ return Stdlib_Option.map(Reference$Reventless.getTarget(param[1]), target => ({
148
+ fieldName: fieldName,
149
+ entity: target.entity,
150
+ plugin: target.plugin
151
+ }));
152
+ });
153
+ return {
154
+ name: variantName,
155
+ schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
156
+ references: references
157
+ };
158
+ };
159
+ switch (v.type) {
160
+ case "string" :
161
+ let variantName = v.const;
162
+ if (variantName !== undefined) {
163
+ return mkDef(variantName, {});
164
+ } else {
165
+ return;
166
+ }
167
+ case "object" :
168
+ let properties = v.properties;
169
+ return Stdlib_Option.flatMap(properties["TAG"], tagSchema => {
170
+ if (tagSchema.type !== "string") {
171
+ return;
172
+ }
173
+ let variantName = tagSchema.const;
174
+ if (variantName !== undefined) {
175
+ return mkDef(variantName, properties);
176
+ }
177
+ });
178
+ default:
179
+ return;
180
+ }
181
+ }
182
+
183
+ function extractEventDefs(eventSchema) {
184
+ if (eventSchema.type === "union") {
185
+ return Stdlib_Array.filterMap(eventSchema.anyOf, toEventDef);
186
+ } else {
187
+ return Stdlib_Option.mapOr(toEventDef(eventSchema), [], def => [def]);
188
+ }
189
+ }
190
+
191
+ function extractErrorDefs(errorSchema) {
192
+ return extractEventDefs(errorSchema).map(param => ({
193
+ name: param.name,
194
+ schema: param.schema,
195
+ references: param.references
196
+ }));
197
+ }
198
+
143
199
  function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionsOpt, extensionPointsOpt, componentChaptersOpt) {
144
200
  let aggregates = aggregatesOpt !== undefined ? aggregatesOpt : [];
145
201
  let readModels = readModelsOpt !== undefined ? readModelsOpt : [];
@@ -268,52 +324,6 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
268
324
  return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandSchema), [], def => [def]);
269
325
  }
270
326
  };
271
- let toEventDef = v => {
272
- let mkDef = (variantName, properties) => {
273
- let references = Stdlib_Array.filterMap(Object.entries(properties), param => {
274
- let fieldName = param[0];
275
- return Stdlib_Option.map(Reference$Reventless.getTarget(param[1]), target => ({
276
- fieldName: fieldName,
277
- entity: target.entity,
278
- plugin: target.plugin
279
- }));
280
- });
281
- return {
282
- name: variantName,
283
- schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
284
- references: references
285
- };
286
- };
287
- switch (v.type) {
288
- case "string" :
289
- let variantName = v.const;
290
- if (variantName !== undefined) {
291
- return mkDef(variantName, {});
292
- } else {
293
- return;
294
- }
295
- case "object" :
296
- let properties = v.properties;
297
- return Stdlib_Option.flatMap(properties["TAG"], tagSchema => {
298
- if (tagSchema.type !== "string") {
299
- return;
300
- }
301
- let variantName = tagSchema.const;
302
- if (variantName !== undefined) {
303
- return mkDef(variantName, properties);
304
- }
305
- });
306
- default:
307
- return;
308
- }
309
- };
310
- let extractEventDefs = eventSchema => {
311
- if (eventSchema.type === "union") {
312
- return Stdlib_Array.filterMap(eventSchema.anyOf, toEventDef);
313
- } else {
314
- return Stdlib_Option.mapOr(toEventDef(eventSchema), [], def => [def]);
315
- }
316
- };
317
327
  let scsProduced = stateChangeSlices.map(SCS => [
318
328
  SCS.Spec.name,
319
329
  qualify(name, DcbTag$Reventless.extractVariantNames(SCS.Spec.eventSchema))
@@ -541,6 +551,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
541
551
  linkedViews: linkedSvsFor(produced),
542
552
  consistencyRead: consistencyReadFor(consumed),
543
553
  events: extractEventDefs(SCS.Spec.eventSchema),
554
+ errors: extractErrorDefs(SCS.Spec.errorSchema),
544
555
  chapter: componentChapters[SCS.Spec.name]
545
556
  };
546
557
  });
@@ -555,6 +566,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
555
566
  linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
556
567
  consistencyRead: undefined,
557
568
  events: extractEventDefs(A.Spec.eventSchema),
569
+ errors: extractErrorDefs(A.Spec.errorSchema),
558
570
  chapter: componentChapters[A.Spec.name]
559
571
  };
560
572
  });
@@ -641,6 +653,9 @@ export {
641
653
  statusFieldFromStateSchema,
642
654
  labelFieldSourceToString,
643
655
  labelFieldsFromStateSchema,
656
+ toEventDef,
657
+ extractEventDefs,
658
+ extractErrorDefs,
644
659
  make,
645
660
  }
646
661
  /* log Not a pure module */
@@ -68,3 +68,118 @@ describe("ResourceAttribution — ambient plugin context", () => {
68
68
  expect(Attribution.current.contents.plugin)->toEqual(None)
69
69
  })
70
70
  })
71
+
72
+ // The bracket only covers what runs synchronously inside a builder's construct.
73
+ // Builders defer part of their work into a `Pulumi.Output.apply`, which runs
74
+ // after `restore` has emptied the context — so a resource created there is
75
+ // attributed to nobody. These pin the mechanism that carries the context across
76
+ // that gap. Each empties the context first, which is exactly the state a
77
+ // deferred callback finds itself in.
78
+
79
+ describe("ResourceAttribution — carrying the context into deferred work", () => {
80
+ testSync("a deferred callback runs under the context it was wrapped in", () => {
81
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
82
+ let observed = ref(None)
83
+ let finish = Attribution.deferred(() => observed := Attribution.current.contents.plugin)
84
+ Attribution.restore(previous)
85
+
86
+ // Construct has returned; nothing is attributed. This is when the apply fires.
87
+ expect(Attribution.current.contents.plugin)->toEqual(None)
88
+ finish()
89
+ expect(observed.contents)->toEqual(Some("Catalog"))
90
+ })
91
+
92
+ testSync("it puts the context back, so the next callback is not attributed to it", () => {
93
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
94
+ let finish = Attribution.deferred(() => ())
95
+ Attribution.restore(previous)
96
+
97
+ finish()
98
+ expect(Attribution.current.contents.plugin)->toEqual(None)
99
+ })
100
+
101
+ // The registries these callbacks land in are module-level and shared, so one
102
+ // apply runs several plugins' finish functions back to back. Attributing them
103
+ // all to whichever ran first is the failure this guards.
104
+ testSync("two plugins deferred from one apply are each attributed to themselves", () => {
105
+ let outer = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
106
+ let catalogSaw = ref(None)
107
+ let catalogFinish = Attribution.deferred(() =>
108
+ catalogSaw := Attribution.current.contents.plugin
109
+ )
110
+ Attribution.restore(outer)
111
+
112
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Ordering")
113
+ let orderingSaw = ref(None)
114
+ let orderingFinish = Attribution.deferred(() =>
115
+ orderingSaw := Attribution.current.contents.plugin
116
+ )
117
+ Attribution.restore(previous)
118
+
119
+ catalogFinish()
120
+ orderingFinish()
121
+ expect((catalogSaw.contents, orderingSaw.contents))->toEqual((
122
+ Some("Catalog"),
123
+ Some("Ordering"),
124
+ ))
125
+ })
126
+
127
+ testSync("a throwing callback still puts the context back", () => {
128
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
129
+ let finish: unit => unit = Attribution.deferred(() =>
130
+ JsError.throwWithMessage("finish blew up")
131
+ )
132
+ Attribution.restore(previous)
133
+
134
+ let threw = try {
135
+ finish()
136
+ false
137
+ } catch {
138
+ | _ => true
139
+ }
140
+ expect(threw)->toBe(true)
141
+ expect(Attribution.current.contents.plugin)->toEqual(None)
142
+ })
143
+
144
+ testSync("within re-enters a captured context for a callback that takes arguments", () => {
145
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
146
+ let captured = Attribution.current.contents
147
+ Attribution.restore(previous)
148
+
149
+ let observed = captured->Attribution.within(() => Attribution.current.contents.plugin)
150
+ expect(observed)->toEqual(Some("Catalog"))
151
+ expect(Attribution.current.contents.plugin)->toEqual(None)
152
+ })
153
+
154
+ testSync("enterCaptured can express the empty context, which enter cannot", () => {
155
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
156
+ let restoredToEmpty = Attribution.enterCaptured({platform: None, plugin: None})
157
+ expect(Attribution.current.contents.plugin)->toEqual(None)
158
+ expect(restoredToEmpty.plugin)->toEqual(Some("Catalog"))
159
+ Attribution.restore(previous)
160
+ })
161
+ })
162
+
163
+ // The combinator being right is not the same as it being wired in. This one
164
+ // exercises the production choke point: a task side-effect handler registered
165
+ // during construct, then run the way `finishTasks` runs it — from an apply,
166
+ // with nothing attributed.
167
+ describe("Builder_Helpers.registerTaskSideEffectHandler — attribution survives the deferral", () => {
168
+ testSync("the registered finish sees the plugin that registered it", () => {
169
+ let before = Builder_Helpers.taskSideEffectFinishFns->Array.length
170
+ let observed = ref(None)
171
+
172
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
173
+ Builder_Helpers.registerTaskSideEffectHandler(
174
+ ~gate=Pulumi.Output.make(),
175
+ ~finish=() => observed := Attribution.current.contents.plugin,
176
+ )
177
+ Attribution.restore(previous)
178
+
179
+ switch Builder_Helpers.taskSideEffectFinishFns->Array.get(before) {
180
+ | Some(finish) => finish()
181
+ | None => JsError.throwWithMessage("handler was not registered")
182
+ }
183
+ expect(observed.contents)->toEqual(Some("Catalog"))
184
+ })
185
+ })
@@ -1,6 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Pulumi from "@pulumi/pulumi";
4
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
3
5
  import * as ComponentType$ReventlessCore from "../src/ComponentType.res.mjs";
6
+ import * as Builder_Helpers$ReventlessCore from "../src/components/Builder_Helpers.res.mjs";
4
7
  import * as ResourceAttribution$ReventlessCore from "../src/ResourceAttribution.res.mjs";
5
8
 
6
9
  globalThis.describe("ResourceAttribution.Scope — wire spellings", () => {
@@ -67,6 +70,109 @@ globalThis.describe("ResourceAttribution — ambient plugin context", () => {
67
70
  });
68
71
  });
69
72
 
73
+ globalThis.describe("ResourceAttribution — carrying the context into deferred work", () => {
74
+ globalThis.test("a deferred callback runs under the context it was wrapped in", () => {
75
+ let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
76
+ let observed = {
77
+ contents: undefined
78
+ };
79
+ let finish = ResourceAttribution$ReventlessCore.deferred(() => {
80
+ observed.contents = ResourceAttribution$ReventlessCore.current.contents.plugin;
81
+ });
82
+ ResourceAttribution$ReventlessCore.restore(previous);
83
+ globalThis.expect(ResourceAttribution$ReventlessCore.current.contents.plugin).toEqual(undefined);
84
+ finish();
85
+ globalThis.expect(observed.contents).toEqual("Catalog");
86
+ });
87
+ globalThis.test("it puts the context back, so the next callback is not attributed to it", () => {
88
+ let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
89
+ let finish = ResourceAttribution$ReventlessCore.deferred(() => {});
90
+ ResourceAttribution$ReventlessCore.restore(previous);
91
+ finish();
92
+ globalThis.expect(ResourceAttribution$ReventlessCore.current.contents.plugin).toEqual(undefined);
93
+ });
94
+ globalThis.test("two plugins deferred from one apply are each attributed to themselves", () => {
95
+ let outer = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
96
+ let catalogSaw = {
97
+ contents: undefined
98
+ };
99
+ let catalogFinish = ResourceAttribution$ReventlessCore.deferred(() => {
100
+ catalogSaw.contents = ResourceAttribution$ReventlessCore.current.contents.plugin;
101
+ });
102
+ ResourceAttribution$ReventlessCore.restore(outer);
103
+ let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Ordering");
104
+ let orderingSaw = {
105
+ contents: undefined
106
+ };
107
+ let orderingFinish = ResourceAttribution$ReventlessCore.deferred(() => {
108
+ orderingSaw.contents = ResourceAttribution$ReventlessCore.current.contents.plugin;
109
+ });
110
+ ResourceAttribution$ReventlessCore.restore(previous);
111
+ catalogFinish();
112
+ orderingFinish();
113
+ globalThis.expect([
114
+ catalogSaw.contents,
115
+ orderingSaw.contents
116
+ ]).toEqual([
117
+ "Catalog",
118
+ "Ordering"
119
+ ]);
120
+ });
121
+ globalThis.test("a throwing callback still puts the context back", () => {
122
+ let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
123
+ let finish = ResourceAttribution$ReventlessCore.deferred(() => Stdlib_JsError.throwWithMessage("finish blew up"));
124
+ ResourceAttribution$ReventlessCore.restore(previous);
125
+ let threw;
126
+ try {
127
+ finish();
128
+ threw = false;
129
+ } catch (exn) {
130
+ threw = true;
131
+ }
132
+ globalThis.expect(threw).toBe(true);
133
+ globalThis.expect(ResourceAttribution$ReventlessCore.current.contents.plugin).toEqual(undefined);
134
+ });
135
+ globalThis.test("within re-enters a captured context for a callback that takes arguments", () => {
136
+ let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
137
+ let captured = ResourceAttribution$ReventlessCore.current.contents;
138
+ ResourceAttribution$ReventlessCore.restore(previous);
139
+ let observed = ResourceAttribution$ReventlessCore.within(captured, () => ResourceAttribution$ReventlessCore.current.contents.plugin);
140
+ globalThis.expect(observed).toEqual("Catalog");
141
+ globalThis.expect(ResourceAttribution$ReventlessCore.current.contents.plugin).toEqual(undefined);
142
+ });
143
+ globalThis.test("enterCaptured can express the empty context, which enter cannot", () => {
144
+ let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
145
+ let restoredToEmpty = ResourceAttribution$ReventlessCore.enterCaptured({
146
+ platform: undefined,
147
+ plugin: undefined
148
+ });
149
+ globalThis.expect(ResourceAttribution$ReventlessCore.current.contents.plugin).toEqual(undefined);
150
+ globalThis.expect(restoredToEmpty.plugin).toEqual("Catalog");
151
+ ResourceAttribution$ReventlessCore.restore(previous);
152
+ });
153
+ });
154
+
155
+ globalThis.describe("Builder_Helpers.registerTaskSideEffectHandler — attribution survives the deferral", () => {
156
+ globalThis.test("the registered finish sees the plugin that registered it", () => {
157
+ let before = Builder_Helpers$ReventlessCore.taskSideEffectFinishFns.length;
158
+ let observed = {
159
+ contents: undefined
160
+ };
161
+ let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
162
+ Builder_Helpers$ReventlessCore.registerTaskSideEffectHandler(Pulumi.output(), () => {
163
+ observed.contents = ResourceAttribution$ReventlessCore.current.contents.plugin;
164
+ });
165
+ ResourceAttribution$ReventlessCore.restore(previous);
166
+ let finish = Builder_Helpers$ReventlessCore.taskSideEffectFinishFns[before];
167
+ if (finish !== undefined) {
168
+ finish();
169
+ } else {
170
+ Stdlib_JsError.throwWithMessage("handler was not registered");
171
+ }
172
+ globalThis.expect(observed.contents).toEqual("Catalog");
173
+ });
174
+ });
175
+
70
176
  let Attribution;
71
177
 
72
178
  export {
@@ -0,0 +1,55 @@
1
+ open JestGlobals
2
+
3
+ // The seam is a single process-global switch, so what is worth pinning is the
4
+ // default (off) and the fact that switching it on does not also register a
5
+ // runtime interceptor — the two are deliberately separate decisions, and
6
+ // conflating them would provision an interceptor path for a hook nobody set.
7
+
8
+ module Interception = QueryInterception
9
+
10
+ describe("QueryInterception", () => {
11
+ testSync("off by default — no registration, nothing provisioned", () => {
12
+ Interception.reset()
13
+ expect(Interception.isEnabled())->toBe(false)
14
+ })
15
+
16
+ testSync("use switches it on for the deployment", () => {
17
+ Interception.reset()
18
+ Interception.use()
19
+ expect(Interception.isEnabled())->toBe(true)
20
+ Interception.reset()
21
+ })
22
+
23
+ testSync("use is idempotent — an extension may call it more than once", () => {
24
+ Interception.reset()
25
+ Interception.use()
26
+ Interception.use()
27
+ expect(Interception.isEnabled())->toBe(true)
28
+ Interception.reset()
29
+ })
30
+
31
+ testSync("reset puts it back, so one test cannot leak into the next", () => {
32
+ Interception.use()
33
+ Interception.reset()
34
+ expect(Interception.isEnabled())->toBe(false)
35
+ })
36
+
37
+ // Provisioning the path and registering the hook are separate on purpose: the
38
+ // framework provisions, the extension decides what runs. Asking for
39
+ // interception must not fabricate an interceptor.
40
+ testSync("switching provisioning on does not register a runtime interceptor", () => {
41
+ QueryDb_Callback.clearQueryInterceptor()
42
+ Interception.use()
43
+ expect(QueryDb_Callback.queryInterceptorHook.contents->Option.isNone)->toBe(true)
44
+ Interception.reset()
45
+ })
46
+
47
+ testSync("and registering a runtime interceptor does not switch provisioning on", () => {
48
+ Interception.reset()
49
+ QueryDb_Callback.registerQueryInterceptor((~identity as _, ~readModelName as _, ~args as _) =>
50
+ Promise.resolve(QueryDb_Callback.Allow)
51
+ )
52
+ expect(Interception.isEnabled())->toBe(false)
53
+ QueryDb_Callback.clearQueryInterceptor()
54
+ })
55
+ })
@@ -0,0 +1,49 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as QueryDb_Callback$ReventlessCore from "../../src/components/QueryDb/QueryDb_Callback.res.mjs";
5
+ import * as QueryInterception$ReventlessCore from "../../src/adapter/QueryInterception/QueryInterception.res.mjs";
6
+
7
+ globalThis.describe("QueryInterception", () => {
8
+ globalThis.test("off by default — no registration, nothing provisioned", () => {
9
+ QueryInterception$ReventlessCore.reset();
10
+ globalThis.expect(QueryInterception$ReventlessCore.isEnabled()).toBe(false);
11
+ });
12
+ globalThis.test("use switches it on for the deployment", () => {
13
+ QueryInterception$ReventlessCore.reset();
14
+ QueryInterception$ReventlessCore.use();
15
+ globalThis.expect(QueryInterception$ReventlessCore.isEnabled()).toBe(true);
16
+ QueryInterception$ReventlessCore.reset();
17
+ });
18
+ globalThis.test("use is idempotent — an extension may call it more than once", () => {
19
+ QueryInterception$ReventlessCore.reset();
20
+ QueryInterception$ReventlessCore.use();
21
+ QueryInterception$ReventlessCore.use();
22
+ globalThis.expect(QueryInterception$ReventlessCore.isEnabled()).toBe(true);
23
+ QueryInterception$ReventlessCore.reset();
24
+ });
25
+ globalThis.test("reset puts it back, so one test cannot leak into the next", () => {
26
+ QueryInterception$ReventlessCore.use();
27
+ QueryInterception$ReventlessCore.reset();
28
+ globalThis.expect(QueryInterception$ReventlessCore.isEnabled()).toBe(false);
29
+ });
30
+ globalThis.test("switching provisioning on does not register a runtime interceptor", () => {
31
+ QueryDb_Callback$ReventlessCore.clearQueryInterceptor();
32
+ QueryInterception$ReventlessCore.use();
33
+ globalThis.expect(Stdlib_Option.isNone(QueryDb_Callback$ReventlessCore.queryInterceptorHook.contents)).toBe(true);
34
+ QueryInterception$ReventlessCore.reset();
35
+ });
36
+ globalThis.test("and registering a runtime interceptor does not switch provisioning on", () => {
37
+ QueryInterception$ReventlessCore.reset();
38
+ QueryDb_Callback$ReventlessCore.registerQueryInterceptor((param, param$1, param$2) => Promise.resolve("Allow"));
39
+ globalThis.expect(QueryInterception$ReventlessCore.isEnabled()).toBe(false);
40
+ QueryDb_Callback$ReventlessCore.clearQueryInterceptor();
41
+ });
42
+ });
43
+
44
+ let Interception;
45
+
46
+ export {
47
+ Interception,
48
+ }
49
+ /* Not a pure module */
@@ -41,6 +41,7 @@ let wbl: writableDef = {
41
41
  producedEventTypes: ["ProductAdded"],
42
42
  consumedEventTypes: [],
43
43
  events: [{name: "ProductAdded", schema: "{}", references: []}],
44
+ errors: [{name: "ProductAlreadyExists", schema: "{}", references: []}],
44
45
  chapter: None,
45
46
  }
46
47
 
@@ -162,6 +163,17 @@ describe("encodePluginStructureEntry", () => {
162
163
  expect(json->String.includes("\"events\":[{\"name\":\"ProductAdded\""))->toEqual(true)
163
164
  )
164
165
 
166
+ // The refusals a caller has to handle travel beside the facts it can observe.
167
+ testSync("encodes writableDef errors", () =>
168
+ expect(json->String.includes("\"errors\":[{\"name\":\"ProductAlreadyExists\""))->toEqual(true)
169
+ )
170
+
171
+ testSync("declares Platform_ErrorDef and the errors field in the shared write-side SDL", () => {
172
+ let sdl = Platform_ComponentDefinitionsApi.sdlTypes->Array.join("\n")
173
+ expect(sdl->String.includes("type Platform_ErrorDef"))->toEqual(true)
174
+ expect(sdl->String.includes("errors: [Platform_ErrorDef!]!"))->toEqual(true)
175
+ })
176
+
165
177
  // Translation-slice externalSystem: absent on the fixture above (both None), so it
166
178
  // must serialize as null — the "no external box" case. The leading field keeps the
167
179
  // two directions apart; `targetName` sits between them and is pinned here too.
@@ -292,6 +304,7 @@ describe("allowedStates + statusField populated", () => {
292
304
  producedEventTypes: [],
293
305
  consumedEventTypes: [],
294
306
  events: [],
307
+ errors: [],
295
308
  chapter: None,
296
309
  }
297
310
 
@@ -325,4 +338,12 @@ describe("allowedStates + statusField populated", () => {
325
338
  testSync("encodes populated statusField as the field name string", () =>
326
339
  expect(json->String.includes("\"statusField\":\"status\""))->toEqual(true)
327
340
  )
341
+
342
+ // "declares no errors" is an empty list, never null — the SDL types `errors` as
343
+ // non-null, and a structure is re-derived on every build, so there is no third
344
+ // "cannot say" state to encode.
345
+ testSync("encodes a component with no declared errors as an empty array, not null", () => {
346
+ expect(json->String.includes("\"errors\":[]"))->toEqual(true)
347
+ expect(json->String.includes("\"errors\":null"))->toEqual(false)
348
+ })
328
349
  })
@@ -61,6 +61,12 @@ let wbl_events = [{
61
61
  references: []
62
62
  }];
63
63
 
64
+ let wbl_errors = [{
65
+ name: "ProductAlreadyExists",
66
+ schema: "{}",
67
+ references: []
68
+ }];
69
+
64
70
  let wbl = {
65
71
  name: "Product",
66
72
  commands: wbl_commands,
@@ -69,6 +75,7 @@ let wbl = {
69
75
  linkedViews: wbl_linkedViews,
70
76
  consistencyRead: undefined,
71
77
  events: wbl_events,
78
+ errors: wbl_errors,
72
79
  chapter: undefined
73
80
  };
74
81
 
@@ -182,6 +189,14 @@ globalThis.describe("encodePluginStructureEntry", () => {
182
189
  globalThis.test("encodes writableDef events", () => {
183
190
  globalThis.expect(json.includes("\"events\":[{\"name\":\"ProductAdded\"")).toEqual(true);
184
191
  });
192
+ globalThis.test("encodes writableDef errors", () => {
193
+ globalThis.expect(json.includes("\"errors\":[{\"name\":\"ProductAlreadyExists\"")).toEqual(true);
194
+ });
195
+ globalThis.test("declares Platform_ErrorDef and the errors field in the shared write-side SDL", () => {
196
+ let sdl = Platform_ComponentDefinitionsApi$ReventlessCore.sdlTypes.join("\n");
197
+ globalThis.expect(sdl.includes("type Platform_ErrorDef")).toEqual(true);
198
+ globalThis.expect(sdl.includes("errors: [Platform_ErrorDef!]!")).toEqual(true);
199
+ });
185
200
  globalThis.test("encodes None outbound externalSystem as null", () => {
186
201
  globalThis.expect(json.includes("\"inboundCommandTypes\":[\"Ship\"],\"targetName\":null,\"externalSystem\":null")).toEqual(true);
187
202
  });
@@ -326,6 +341,7 @@ globalThis.describe("allowedStates + statusField populated", () => {
326
341
  let wblWithStates_consumedEventTypes = [];
327
342
  let wblWithStates_linkedViews = ["Plugin"];
328
343
  let wblWithStates_events = [];
344
+ let wblWithStates_errors = [];
329
345
  let wblWithStates = {
330
346
  name: "Plugin",
331
347
  commands: wblWithStates_commands,
@@ -334,6 +350,7 @@ globalThis.describe("allowedStates + statusField populated", () => {
334
350
  linkedViews: wblWithStates_linkedViews,
335
351
  consistencyRead: undefined,
336
352
  events: wblWithStates_events,
353
+ errors: wblWithStates_errors,
337
354
  chapter: undefined
338
355
  };
339
356
  let structureWithStates_readModels = [qblWithStatus];
@@ -367,6 +384,10 @@ globalThis.describe("allowedStates + statusField populated", () => {
367
384
  globalThis.test("encodes populated statusField as the field name string", () => {
368
385
  globalThis.expect(json.includes("\"statusField\":\"status\"")).toEqual(true);
369
386
  });
387
+ globalThis.test("encodes a component with no declared errors as an empty array, not null", () => {
388
+ globalThis.expect(json.includes("\"errors\":[]")).toEqual(true);
389
+ globalThis.expect(json.includes("\"errors\":null")).toEqual(false);
390
+ });
370
391
  });
371
392
 
372
393
  export {