@reventlessdev/reventless-core 3.0.0-alpha.212 → 3.0.0-alpha.214

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 (30) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +6 -6
  3. package/src/Projection.res +5 -2
  4. package/src/Projection.res.mjs +2 -1
  5. package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +165 -0
  6. package/src/adapter/EventLogProvisioning/EventLogProvisioning.res.mjs +49 -0
  7. package/src/components/AutomationSlice/AutomationSlice_Callback.res +30 -0
  8. package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +12 -0
  9. package/src/components/Dcb/Dcb_Builder.res +6 -2
  10. package/src/components/Dcb/Dcb_Builder.res.mjs +2 -2
  11. package/src/components/DcbEventLog/DcbEventLog_Builder.res +17 -4
  12. package/src/components/DcbEventLog/DcbEventLog_Builder.res.mjs +9 -5
  13. package/src/components/EventLog/EventLog_Builder.res +10 -2
  14. package/src/components/EventLog/EventLog_Builder.res.mjs +4 -1
  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/adapter/EventLogProvisioningTest.res +207 -0
  26. package/tests/adapter/EventLogProvisioningTest.res.mjs +150 -0
  27. package/tests/api/GraphQL_FragmentGeneratorTest.res +40 -0
  28. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +35 -0
  29. package/tests/logger/LogFormatTest.res +42 -0
  30. package/tests/logger/LogFormatTest.res.mjs +37 -0
@@ -788,9 +788,10 @@ let clearOnPluginDeployed = () => {
788
788
  // ---------------------------------------------------------------------------
789
789
  // Deploy-time offload hook — content-addresses a large pluginDefinition field
790
790
  // to an object store and returns the reference. Set by a provider that can
791
- // write objects (AWS creates a BucketObject); unset on the local platform, so
792
- // the fields stay Inline. `bytes` is the already-serialized JSON of the value;
793
- // the provider hashes it, stores it at `sha256/<hash>`, and returns the ref.
791
+ // write objects (AWS creates a BucketObject, local writes to its process-local
792
+ // object store); when unset the fields stay Inline. `bytes` is the
793
+ // already-serialized JSON of the value; the provider hashes it, stores it at
794
+ // `sha256/<hash>`, and returns the ref.
794
795
  // Fired synchronously at graph-construction time (the value is concrete and a
795
796
  // resource cannot be created inside a Pulumi.Output.apply).
796
797
  // ---------------------------------------------------------------------------
@@ -806,6 +807,23 @@ let clearOffload = () => {
806
807
  offloadHook.contents = None
807
808
  }
808
809
 
810
+ /**
811
+ Carry a large `pluginDefinition` field by reference when a provider registered an
812
+ offload hook, inline otherwise.
813
+
814
+ One implementation for both construction sites — `Plugin_Builder`'s deploy-time
815
+ definition and the local platform's synthetic one — so a provider that offloads
816
+ cannot end up offloading through one path and inlining through the other.
817
+ */
818
+ let offloadPayload = (value: 'a, ~schema: S.t<'a>, ~store: string): Reventless.Offload.payload<'a> =>
819
+ switch offloadHook.contents {
820
+ | Some(hook) =>
821
+ Reventless.Offload.Offloaded(
822
+ hook(~store, ~bytes=value->S.reverseConvertToJsonStringOrThrow(schema)),
823
+ )
824
+ | None => Reventless.Offload.Inline(value)
825
+ }
826
+
809
827
  // ---------------------------------------------------------------------------
810
828
  // Platform-deployed hook — fires after deployPlatform / makePlatform
811
829
  // completes, providing platform-level metadata with resolved values.
@@ -430,6 +430,21 @@ function clearOffload() {
430
430
  offloadHook.contents = undefined;
431
431
  }
432
432
 
433
+ function offloadPayload(value, schema, store) {
434
+ let hook = offloadHook.contents;
435
+ if (hook !== undefined) {
436
+ return {
437
+ TAG: "Offloaded",
438
+ _0: hook(store, S.reverseConvertToJsonStringOrThrow(value, schema, undefined))
439
+ };
440
+ } else {
441
+ return {
442
+ TAG: "Inline",
443
+ _0: value
444
+ };
445
+ }
446
+ }
447
+
433
448
  let onPlatformDeployedHook = {
434
449
  contents: undefined
435
450
  };
@@ -1030,6 +1045,7 @@ export {
1030
1045
  offloadHook,
1031
1046
  registerOffload,
1032
1047
  clearOffload,
1048
+ offloadPayload,
1033
1049
  onPlatformDeployedHook,
1034
1050
  lastPlatformDeployedInfo,
1035
1051
  registerOnPlatformDeployed,
@@ -28,6 +28,41 @@
28
28
  // styling primitive through `LogFormat.bold` rather than the spec namespace.
29
29
  let bold = Reventless.AnsiStyle.bold
30
30
 
31
+ // ─── Value truncation ────────────────────────────────────────────────────────
32
+
33
+ @val external logEnv: dict<string> = "process.env"
34
+
35
+ /**
36
+ Per-value character budget for rendered payloads, before the rest is elided.
37
+
38
+ A detail line exists to say *which* command or event this is, not to reproduce
39
+ it: a single field can be arbitrarily large (a plugin structure, a pasted
40
+ document, an inline offload payload), and one of those turns a log into
41
+ something no one reads. Truncation is per value rather than per line so a large
42
+ field costs its neighbours nothing — the small fields that identify the message
43
+ stay visible next to it.
44
+
45
+ `REVENTLESS_LOG_MAX_VALUE` overrides the budget; `0` disables truncation for a
46
+ session that genuinely needs the whole payload.
47
+ */
48
+ let maxValueChars =
49
+ logEnv
50
+ ->Dict.get("REVENTLESS_LOG_MAX_VALUE")
51
+ ->Option.flatMap(v => Int.fromString(v))
52
+ ->Option.getOr(512)
53
+
54
+ /** `s` capped at `maxValueChars`, with what was dropped named rather than
55
+ silently missing — a truncated line must not read like a complete one. */
56
+ let truncate = (s: string): string => {
57
+ let len = s->String.length
58
+ if maxValueChars <= 0 || len <= maxValueChars {
59
+ s
60
+ } else {
61
+ `${s->String.slice(~start=0, ~end=maxValueChars)}…(+${(len - maxValueChars)
62
+ ->Int.toString} chars)`
63
+ }
64
+ }
65
+
31
66
  // ─── Commands ────────────────────────────────────────────────────────────────
32
67
 
33
68
  // "Add" (bold)
@@ -46,6 +81,16 @@ let cmdSummary = (msg: Message.commandJson): string =>
46
81
  let cmdSummaries = (msgs: array<Message.commandJson>): string =>
47
82
  `[${msgs->Array.map(cmdSummary)->Array.join(",")}]`
48
83
 
84
+ /**
85
+ `JSON.stringify` is typed as total but returns `undefined` — not a string — for
86
+ the one value `JSON.t` cannot represent: the `undefined` a ReScript record's
87
+ *absent* optional field leaves behind as an own property of the JS object. Read
88
+ the result as nullable so an absent field is dropped instead of reaching a
89
+ string function as `undefined`. An explicit `null` still stringifies to `"null"`
90
+ and keeps printing.
91
+ */
92
+ external stringifiedOrAbsent: string => Nullable.t<string> = "%identity"
93
+
49
94
  // Strip TAG from a variant JSON, return remaining fields as a compact string.
50
95
  // {"TAG":"Add","name":"Cat 1"} → {name:"Cat 1"}
51
96
  // "Add" (bare string variant) → ""
@@ -56,7 +101,13 @@ let variantFields = (json: JSON.t): string =>
56
101
  dict
57
102
  ->Dict.toArray
58
103
  ->Array.filter(((k, _)) => k != "TAG")
59
- ->Array.map(((k, v)) => `${k}:${v->JSON.stringify}`)
104
+ ->Array.filterMap(((k, v)) =>
105
+ v
106
+ ->JSON.stringify
107
+ ->stringifiedOrAbsent
108
+ ->Nullable.toOption
109
+ ->Option.map(s => `${k}:${s->truncate}`)
110
+ )
60
111
  ->Array.join(",")
61
112
  fields == "" ? "" : `, {${fields}}`
62
113
  | _ => ""
@@ -1,11 +1,25 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
3
4
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
6
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
9
  import * as AnsiStyle$Reventless from "@reventlessdev/reventless-spec/src/AnsiStyle.res.mjs";
7
10
  import * as Message$ReventlessCore from "../Message.res.mjs";
8
11
 
12
+ let maxValueChars = Stdlib_Option.getOr(Stdlib_Option.flatMap(process.env["REVENTLESS_LOG_MAX_VALUE"], v => Stdlib_Int.fromString(v, undefined)), 512);
13
+
14
+ function truncate(s) {
15
+ let len = s.length;
16
+ if (maxValueChars <= 0 || len <= maxValueChars) {
17
+ return s;
18
+ } else {
19
+ return s.slice(0, maxValueChars) + `…(+` + (len - maxValueChars | 0).toString() + ` chars)`;
20
+ }
21
+ }
22
+
9
23
  function cmdName(msg) {
10
24
  return AnsiStyle$Reventless.bold(Message$ReventlessCore.variantNameOfJson(msg.commandJson));
11
25
  }
@@ -26,7 +40,10 @@ function variantFields(json) {
26
40
  if (typeof json !== "object" || json === null || Array.isArray(json)) {
27
41
  return "";
28
42
  }
29
- let fields = Object.entries(json).filter(param => param[0] !== "TAG").map(param => param[0] + `:` + JSON.stringify(param[1])).join(",");
43
+ let fields = Stdlib_Array.filterMap(Object.entries(json).filter(param => param[0] !== "TAG"), param => {
44
+ let k = param[0];
45
+ return Stdlib_Option.map(Primitive_option.fromNullable(JSON.stringify(param[1])), s => k + `:` + truncate(s));
46
+ }).join(",");
30
47
  if (fields === "") {
31
48
  return "";
32
49
  } else {
@@ -220,6 +237,8 @@ let fmtActions = actionNames;
220
237
 
221
238
  export {
222
239
  bold,
240
+ maxValueChars,
241
+ truncate,
223
242
  cmdName,
224
243
  cmdNames,
225
244
  cmdSummary,
@@ -254,4 +273,4 @@ export {
254
273
  fmtAction,
255
274
  fmtActions,
256
275
  }
257
- /* Stdlib_JsExn Not a pure module */
276
+ /* maxValueChars Not a pure module */
@@ -0,0 +1,38 @@
1
+ // Where a declared object store's keys are rooted.
2
+ //
3
+ // Lives in core beside StorePrefixCollision, and for the same reason: the rule
4
+ // is about a namespace, not about a cloud. The deployed platform roots an S3
5
+ // store's keys here, the local platform roots its filesystem store's keys here,
6
+ // and a reset attributes an object to the plugin that declared its store by
7
+ // reading the prefix back off the key. Three readers of one rule — so it is one
8
+ // function, not three strings that happen to agree today.
9
+ //
10
+ // Qualifying by plugin is what makes the prefix an identity rather than a label:
11
+ // two plugins may both declare a store called `images`, and only the qualified
12
+ // form keeps their objects apart (which is the collision StorePrefixCollision
13
+ // refuses when it cannot).
14
+
15
+ /** The prefix a store's object keys are rooted at: `{plugin}/{store}`.
16
+
17
+ A stored ref is independent of whether the store got its own bucket or a
18
+ prefix inside a shared one — refs live in an append-only log, so one that
19
+ encoded its physical layout would be unrewritable and environment-specific. */
20
+ let keyPrefixFor = (~plugin: string, ~store: string): string => `${plugin}/${store}`
21
+
22
+ /** The same prefix from the qualified `{plugin}.{store}` key that a
23
+ `pluginStructure.requiredStores` entry carries and that the `Upload_Presign`
24
+ mutation's `store` argument names.
25
+
26
+ Splits on the FIRST `.` — a store name may contain dots, a plugin name may
27
+ not. An unqualified key (no `.`) is returned as-is rather than guessed at:
28
+ it is not a `{plugin}.{store}` and inventing a plugin for it would attribute
29
+ objects to a plugin that never declared them. */
30
+ let prefixOfQualified = (qualified: string): string =>
31
+ switch qualified->String.indexOfOpt(".") {
32
+ | Some(i) =>
33
+ keyPrefixFor(
34
+ ~plugin=qualified->String.slice(~start=0, ~end=i),
35
+ ~store=qualified->String.slice(~start=i + 1, ~end=qualified->String.length),
36
+ )
37
+ | None => qualified
38
+ }
@@ -0,0 +1,22 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
4
+
5
+ function keyPrefixFor(plugin, store) {
6
+ return plugin + `/` + store;
7
+ }
8
+
9
+ function prefixOfQualified(qualified) {
10
+ let i = Stdlib_String.indexOfOpt(qualified, ".");
11
+ if (i !== undefined) {
12
+ return keyPrefixFor(qualified.slice(0, i), qualified.slice(i + 1 | 0, qualified.length));
13
+ } else {
14
+ return qualified;
15
+ }
16
+ }
17
+
18
+ export {
19
+ keyPrefixFor,
20
+ prefixOfQualified,
21
+ }
22
+ /* No side effect */
@@ -0,0 +1,207 @@
1
+ open JestGlobals
2
+
3
+ // The registry only forwards `~resources` and `~opts`; it never reads their
4
+ // fields. A structural stand-in keeps @pulumi/pulumi (whose Output.make would
5
+ // build a real resource) out of the Jest run — its ESM entry can't be imported
6
+ // under Jest. Same reason as MonitoringTest's stub.
7
+ let stubResource: ReventlessInfra.Adapter.resource = %raw(`{}`)
8
+ let noOpts: Pulumi.CustomResourceOptions.t = {}
9
+
10
+ let classicOwner: ResourceAttribution.owner = {kind: ComponentType.Aggregate, name: "Product"}
11
+ let dcbOwner: ResourceAttribution.owner = {kind: ComponentType.Plugin, name: "Catalog"}
12
+
13
+ describe("EventLogProvisioning", () => {
14
+ testSync("Noop default: notify is silent and does not throw", () => {
15
+ // No backend registered — the default Noop backend swallows the call.
16
+ EventLogProvisioning.notify(
17
+ ~logStyle=Classic,
18
+ ~name="ProductEventLog",
19
+ ~owner=Some(classicOwner),
20
+ ~resources=[],
21
+ ~opts=noOpts,
22
+ )
23
+ expect(true)->toBe(true)
24
+ })
25
+
26
+ testSync("use registers a backend that receives every notify, style and name", () => {
27
+ let recorded: array<(EventLogProvisioning.logStyle, string)> = []
28
+ module Recorder: EventLogProvisioning.Backend = {
29
+ let onProvisioned = (
30
+ ~logStyle,
31
+ ~name,
32
+ ~owner as _,
33
+ ~plugin as _,
34
+ ~platform as _,
35
+ ~resources as _,
36
+ ~opts as _,
37
+ ) => recorded->Array.push((logStyle, name))
38
+ }
39
+ EventLogProvisioning.use(module(Recorder: EventLogProvisioning.Backend))
40
+
41
+ EventLogProvisioning.notify(
42
+ ~logStyle=Classic,
43
+ ~name="ProductEventLog",
44
+ ~owner=Some(classicOwner),
45
+ ~resources=[],
46
+ ~opts=noOpts,
47
+ )
48
+ EventLogProvisioning.notify(
49
+ ~logStyle=Dcb,
50
+ ~name="CatalogDcbEventLog",
51
+ ~owner=Some(dcbOwner),
52
+ ~resources=[],
53
+ ~opts=noOpts,
54
+ )
55
+
56
+ expect(recorded)->toEqual([
57
+ (EventLogProvisioning.Classic, "ProductEventLog"),
58
+ (EventLogProvisioning.Dcb, "CatalogDcbEventLog"),
59
+ ])
60
+
61
+ // Restore the default so registry state doesn't leak to other cases.
62
+ EventLogProvisioning.reset()
63
+ })
64
+
65
+ testSync("notify forwards the owner and the adapter's resources unchanged", () => {
66
+ let seen: ref<
67
+ option<(option<ResourceAttribution.owner>, array<ReventlessInfra.Adapter.resource>)>,
68
+ > = ref(None)
69
+ module Capture: EventLogProvisioning.Backend = {
70
+ let onProvisioned = (
71
+ ~logStyle as _,
72
+ ~name as _,
73
+ ~owner,
74
+ ~plugin as _,
75
+ ~platform as _,
76
+ ~resources,
77
+ ~opts as _,
78
+ ) => seen := Some((owner, resources))
79
+ }
80
+ EventLogProvisioning.use(module(Capture: EventLogProvisioning.Backend))
81
+
82
+ EventLogProvisioning.notify(
83
+ ~logStyle=Classic,
84
+ ~name="ProductEventLog",
85
+ ~owner=Some(classicOwner),
86
+ ~resources=[stubResource],
87
+ ~opts=noOpts,
88
+ )
89
+ switch seen.contents {
90
+ | Some((owner, resources)) =>
91
+ expect(owner)->toEqual(Some(classicOwner))
92
+ expect(resources)->toHaveLength(1)
93
+ | None => expect("no notify received")->toBe("a notify")
94
+ }
95
+
96
+ EventLogProvisioning.reset()
97
+ })
98
+
99
+ testSync("a Postgres-style log with no resources still fires — the contract is uniform", () => {
100
+ // Postgres and in-memory adapters return `resources: []`. Backends that only
101
+ // implement a DynamoDB arm ignore those; the seam does not filter for them.
102
+ let styles: array<EventLogProvisioning.logStyle> = []
103
+ module Recorder: EventLogProvisioning.Backend = {
104
+ let onProvisioned = (
105
+ ~logStyle,
106
+ ~name as _,
107
+ ~owner as _,
108
+ ~plugin as _,
109
+ ~platform as _,
110
+ ~resources,
111
+ ~opts as _,
112
+ ) =>
113
+ if resources->Array.length == 0 {
114
+ styles->Array.push(logStyle)
115
+ }
116
+ }
117
+ EventLogProvisioning.use(module(Recorder: EventLogProvisioning.Backend))
118
+
119
+ EventLogProvisioning.notify(
120
+ ~logStyle=Dcb,
121
+ ~name="CatalogDcbEventLog",
122
+ ~owner=Some(dcbOwner),
123
+ ~resources=[],
124
+ ~opts=noOpts,
125
+ )
126
+
127
+ expect(styles)->toEqual([EventLogProvisioning.Dcb])
128
+ EventLogProvisioning.reset()
129
+ })
130
+
131
+ testSync("notify delivers the ambient plugin/platform inside a construct scope, None outside", () => {
132
+ let seen: array<(option<string>, option<string>)> = []
133
+ module OwnerCapture: EventLogProvisioning.Backend = {
134
+ let onProvisioned = (
135
+ ~logStyle as _,
136
+ ~name as _,
137
+ ~owner as _,
138
+ ~plugin,
139
+ ~platform,
140
+ ~resources as _,
141
+ ~opts as _,
142
+ ) => seen->Array.push((plugin, platform))
143
+ }
144
+ EventLogProvisioning.use(module(OwnerCapture: EventLogProvisioning.Backend))
145
+
146
+ // Outside any plugin construct: both None.
147
+ EventLogProvisioning.notify(
148
+ ~logStyle=Classic,
149
+ ~name="ProductEventLog",
150
+ ~owner=Some(classicOwner),
151
+ ~resources=[],
152
+ ~opts=noOpts,
153
+ )
154
+
155
+ // Inside a construct scope (what Plugin_Builder.construct establishes): both delivered.
156
+ let prev = ResourceAttribution.enter(~platform="online-shop", ~plugin="Catalog")
157
+ EventLogProvisioning.notify(
158
+ ~logStyle=Dcb,
159
+ ~name="CatalogDcbEventLog",
160
+ ~owner=Some(dcbOwner),
161
+ ~resources=[],
162
+ ~opts=noOpts,
163
+ )
164
+ ResourceAttribution.restore(prev)
165
+
166
+ // After restore: back to None.
167
+ EventLogProvisioning.notify(
168
+ ~logStyle=Classic,
169
+ ~name="OrderEventLog",
170
+ ~owner=None,
171
+ ~resources=[],
172
+ ~opts=noOpts,
173
+ )
174
+
175
+ expect(seen)->toEqual([(None, None), (Some("Catalog"), Some("online-shop")), (None, None)])
176
+
177
+ EventLogProvisioning.reset()
178
+ })
179
+
180
+ testSync("use is single-slot: a second registration throws rather than silently winning", () => {
181
+ module First: EventLogProvisioning.Backend = {
182
+ let onProvisioned = (
183
+ ~logStyle as _,
184
+ ~name as _,
185
+ ~owner as _,
186
+ ~plugin as _,
187
+ ~platform as _,
188
+ ~resources as _,
189
+ ~opts as _,
190
+ ) => ()
191
+ }
192
+ EventLogProvisioning.use(module(First: EventLogProvisioning.Backend))
193
+
194
+ let threw = try {
195
+ EventLogProvisioning.use(module(First: EventLogProvisioning.Backend))
196
+ false
197
+ } catch {
198
+ | _ => true
199
+ }
200
+ expect(threw)->toBe(true)
201
+
202
+ // reset clears the slot, so a later registration succeeds again.
203
+ EventLogProvisioning.reset()
204
+ EventLogProvisioning.use(module(First: EventLogProvisioning.Backend))
205
+ EventLogProvisioning.reset()
206
+ })
207
+ })
@@ -0,0 +1,150 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as ResourceAttribution$ReventlessCore from "../../src/ResourceAttribution.res.mjs";
4
+ import * as EventLogProvisioning$ReventlessCore from "../../src/adapter/EventLogProvisioning/EventLogProvisioning.res.mjs";
5
+
6
+ let stubResource = {};
7
+
8
+ let noOpts = {};
9
+
10
+ let classicOwner = {
11
+ kind: "Aggregate",
12
+ name: "Product"
13
+ };
14
+
15
+ let dcbOwner = {
16
+ kind: "Plugin",
17
+ name: "Catalog"
18
+ };
19
+
20
+ globalThis.describe("EventLogProvisioning", () => {
21
+ globalThis.test("Noop default: notify is silent and does not throw", () => {
22
+ EventLogProvisioning$ReventlessCore.notify("Classic", "ProductEventLog", classicOwner, [], noOpts);
23
+ globalThis.expect(true).toBe(true);
24
+ });
25
+ globalThis.test("use registers a backend that receives every notify, style and name", () => {
26
+ let recorded = [];
27
+ let onProvisioned = (logStyle, name, param, param$1, param$2, param$3, param$4) => {
28
+ recorded.push([
29
+ logStyle,
30
+ name
31
+ ]);
32
+ };
33
+ let Recorder = {
34
+ onProvisioned: onProvisioned
35
+ };
36
+ EventLogProvisioning$ReventlessCore.use(Recorder);
37
+ EventLogProvisioning$ReventlessCore.notify("Classic", "ProductEventLog", classicOwner, [], noOpts);
38
+ EventLogProvisioning$ReventlessCore.notify("Dcb", "CatalogDcbEventLog", dcbOwner, [], noOpts);
39
+ globalThis.expect(recorded).toEqual([
40
+ [
41
+ "Classic",
42
+ "ProductEventLog"
43
+ ],
44
+ [
45
+ "Dcb",
46
+ "CatalogDcbEventLog"
47
+ ]
48
+ ]);
49
+ EventLogProvisioning$ReventlessCore.reset();
50
+ });
51
+ globalThis.test("notify forwards the owner and the adapter's resources unchanged", () => {
52
+ let seen = {
53
+ contents: undefined
54
+ };
55
+ let onProvisioned = (param, param$1, owner, param$2, param$3, resources, param$4) => {
56
+ seen.contents = [
57
+ owner,
58
+ resources
59
+ ];
60
+ };
61
+ let Capture = {
62
+ onProvisioned: onProvisioned
63
+ };
64
+ EventLogProvisioning$ReventlessCore.use(Capture);
65
+ EventLogProvisioning$ReventlessCore.notify("Classic", "ProductEventLog", classicOwner, [stubResource], noOpts);
66
+ let match = seen.contents;
67
+ if (match !== undefined) {
68
+ globalThis.expect(match[0]).toEqual(classicOwner);
69
+ globalThis.expect(match[1]).toHaveLength(1);
70
+ } else {
71
+ globalThis.expect("no notify received").toBe("a notify");
72
+ }
73
+ EventLogProvisioning$ReventlessCore.reset();
74
+ });
75
+ globalThis.test("a Postgres-style log with no resources still fires — the contract is uniform", () => {
76
+ let styles = [];
77
+ let onProvisioned = (logStyle, param, param$1, param$2, param$3, resources, param$4) => {
78
+ if (resources.length === 0) {
79
+ styles.push(logStyle);
80
+ return;
81
+ }
82
+ };
83
+ let Recorder = {
84
+ onProvisioned: onProvisioned
85
+ };
86
+ EventLogProvisioning$ReventlessCore.use(Recorder);
87
+ EventLogProvisioning$ReventlessCore.notify("Dcb", "CatalogDcbEventLog", dcbOwner, [], noOpts);
88
+ globalThis.expect(styles).toEqual(["Dcb"]);
89
+ EventLogProvisioning$ReventlessCore.reset();
90
+ });
91
+ globalThis.test("notify delivers the ambient plugin/platform inside a construct scope, None outside", () => {
92
+ let seen = [];
93
+ let onProvisioned = (param, param$1, param$2, plugin, platform, param$3, param$4) => {
94
+ seen.push([
95
+ plugin,
96
+ platform
97
+ ]);
98
+ };
99
+ let OwnerCapture = {
100
+ onProvisioned: onProvisioned
101
+ };
102
+ EventLogProvisioning$ReventlessCore.use(OwnerCapture);
103
+ EventLogProvisioning$ReventlessCore.notify("Classic", "ProductEventLog", classicOwner, [], noOpts);
104
+ let prev = ResourceAttribution$ReventlessCore.enter("online-shop", "Catalog");
105
+ EventLogProvisioning$ReventlessCore.notify("Dcb", "CatalogDcbEventLog", dcbOwner, [], noOpts);
106
+ ResourceAttribution$ReventlessCore.restore(prev);
107
+ EventLogProvisioning$ReventlessCore.notify("Classic", "OrderEventLog", undefined, [], noOpts);
108
+ globalThis.expect(seen).toEqual([
109
+ [
110
+ undefined,
111
+ undefined
112
+ ],
113
+ [
114
+ "Catalog",
115
+ "online-shop"
116
+ ],
117
+ [
118
+ undefined,
119
+ undefined
120
+ ]
121
+ ]);
122
+ EventLogProvisioning$ReventlessCore.reset();
123
+ });
124
+ globalThis.test("use is single-slot: a second registration throws rather than silently winning", () => {
125
+ let onProvisioned = (param, param$1, param$2, param$3, param$4, param$5, param$6) => {};
126
+ let First = {
127
+ onProvisioned: onProvisioned
128
+ };
129
+ EventLogProvisioning$ReventlessCore.use(First);
130
+ let threw;
131
+ try {
132
+ EventLogProvisioning$ReventlessCore.use(First);
133
+ threw = false;
134
+ } catch (exn) {
135
+ threw = true;
136
+ }
137
+ globalThis.expect(threw).toBe(true);
138
+ EventLogProvisioning$ReventlessCore.reset();
139
+ EventLogProvisioning$ReventlessCore.use(First);
140
+ EventLogProvisioning$ReventlessCore.reset();
141
+ });
142
+ });
143
+
144
+ export {
145
+ stubResource,
146
+ noOpts,
147
+ classicOwner,
148
+ dcbOwner,
149
+ }
150
+ /* 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
+ })