@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.
- package/CHANGELOG.md +18 -0
- package/package.json +6 -6
- package/src/ResourceAttribution.res +60 -4
- package/src/ResourceAttribution.res.mjs +26 -0
- package/src/adapter/QueryInterception/QueryInterception.res +74 -0
- package/src/adapter/QueryInterception/QueryInterception.res.mjs +26 -0
- package/src/adapter/ResourceAttribution_Deploytime.res +43 -0
- package/src/adapter/ResourceAttribution_Deploytime.res.mjs +20 -0
- package/src/admin/Platform_Admin_Structure.res +4 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +5 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +16 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +24 -1
- package/src/components/Aggregate/Aggregate_Callback.res +14 -2
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +3 -3
- package/src/components/Builder_Helpers.res +21 -3
- package/src/components/Builder_Helpers.res.mjs +4 -3
- package/src/components/CommandTopic/CommandTopic.res.mjs +12 -0
- package/src/components/CommandTopic/CommandTopic_Helpers.res +88 -2
- package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +48 -2
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +4 -1
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res.mjs +2 -1
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +13 -1
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
- package/src/plugin/component/Plugin_Builder.res +6 -1
- package/src/plugin/component/Plugin_Builder.res.mjs +4 -2
- package/src/plugin/component/Plugin_Helpers.res +7 -2
- package/src/plugin/component/Plugin_Helpers.res.mjs +4 -3
- package/src/plugin/component/Plugin_Structure.res +66 -51
- package/src/plugin/component/Plugin_Structure.res.mjs +61 -46
- package/tests/ResourceAttributionTest.res +115 -0
- package/tests/ResourceAttributionTest.res.mjs +106 -0
- package/tests/adapter/QueryInterceptionTest.res +55 -0
- package/tests/adapter/QueryInterceptionTest.res.mjs +49 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +21 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res +258 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +372 -0
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +9 -1
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +3 -3
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +1 -0
- package/tests/plugin/PluginStructureTest.res +27 -0
- package/tests/plugin/PluginStructureTest.res.mjs +27 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +3 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -1
- 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 {
|