@reventlessdev/reventless-core 3.0.0-alpha.198 → 3.0.0-alpha.200
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 +35 -0
- package/package.json +6 -6
- package/src/admin/Platform_AdminApi.res +23 -0
- package/src/admin/Platform_AdminApi.res.mjs +12 -0
- package/src/components/Task/Task_Builder.res +75 -4
- package/src/components/Task/Task_Builder.res.mjs +33 -1
- package/src/plugin/component/Plugin_Structure.res +8 -5
- package/src/util/StorePrefixCollision.res +96 -0
- package/src/util/StorePrefixCollision.res.mjs +55 -0
- package/tests/StorePrefixCollisionTest.res +160 -0
- package/tests/StorePrefixCollisionTest.res.mjs +174 -0
- package/tests/TaskBucketNamingTest.res +97 -0
- package/tests/TaskBucketNamingTest.res.mjs +60 -0
- package/tests/api/SuryToJsonSchemaTest.res +46 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +39 -0
- package/tests/fixtures/plugin-lifecycle/2026-07-22-versionconnected-no-requiredstores.json +1 -0
- package/tests/fixtures/plugin-lifecycle/2026-07-22-versionsuperseded-nested-definitions.json +1 -0
- package/tests/fixtures/plugin-lifecycle/2026-07-28-versionconnected-requiredstores-only.json +1 -0
- package/tests/fixtures/plugin-lifecycle/2026-07-30-versionconnected-declarations-without-annotation.json +1 -0
- package/tests/fixtures/plugin-lifecycle/2026-07-30-versiondisconnected-declarations-without-annotation.json +1 -0
- package/tests/fixtures/plugin-lifecycle/README.md +36 -0
- package/tests/plugin/PluginDefinitionRequiredScalarsTest.res +42 -0
- package/tests/plugin/PluginDefinitionRequiredScalarsTest.res.mjs +45 -0
- package/tests/plugin/PluginDefinitionScalars.res +78 -0
- package/tests/plugin/PluginDefinitionScalars.res.mjs +110 -0
- package/tests/plugin/PluginLifecycleCorpus.res +42 -0
- package/tests/plugin/PluginLifecycleCorpus.res.mjs +39 -0
- package/tests/plugin/PluginLifecycleCorpusTest.res +50 -0
- package/tests/plugin/PluginLifecycleCorpusTest.res.mjs +42 -0
- package/tests/plugin/PluginStructureTest.res +3 -3
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +103 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Frozen-payload compatibility corpus for the Plugin lifecycle aggregate.
|
|
2
|
+
//
|
|
3
|
+
// The Plugin aggregate replays its own event log before every decision, so ONE
|
|
4
|
+
// undecodable stored event freezes Heartbeat / Redetect / Connect for that plugin
|
|
5
|
+
// permanently — and it does so silently, surfacing only as a registration that
|
|
6
|
+
// stops tracking deploys. See docs/analysis/plugin-definition-schema-evolution-wedge.md.
|
|
7
|
+
//
|
|
8
|
+
// The two hand-constructed cases in MessageTest build a `pluginDefinition` from the
|
|
9
|
+
// CURRENT ReScript type and then strip keys. That literal recompiles with every
|
|
10
|
+
// schema change, so it can only test the fields whoever wrote it thought to strip —
|
|
11
|
+
// which is why it did not catch `annotation: string`. These fixtures are frozen JSON
|
|
12
|
+
// captured off a deployed log and are never regenerated; see the corpus README.
|
|
13
|
+
//
|
|
14
|
+
// What turns this suite red is a change no amount of healing can absorb: a field
|
|
15
|
+
// whose TYPE changed under stored data (`float` → an object, say), a `bigint`, or a
|
|
16
|
+
// genuinely corrupt payload. It names the fixture and the sury field path. The fix is
|
|
17
|
+
// to make the change decodable — never to re-cut the fixtures.
|
|
18
|
+
//
|
|
19
|
+
// A newly added required *scalar* does NOT land here: `fillMissingDefaults` invents a
|
|
20
|
+
// value for it, so these still decode. That case is caught by the sibling
|
|
21
|
+
// PluginDefinitionRequiredScalarsTest, at the declaration site.
|
|
22
|
+
|
|
23
|
+
open JestGlobals
|
|
24
|
+
|
|
25
|
+
let entries = PluginLifecycleCorpus.entries
|
|
26
|
+
|
|
27
|
+
// Mirrors EventLog_Operations.decodeEvent: reassemble the stored `{event, data}`
|
|
28
|
+
// pair into a variant and decode it — the exact call replay makes.
|
|
29
|
+
let decodeStored = (entry: PluginLifecycleCorpus.entry) =>
|
|
30
|
+
Message.combineMessage(entry.event, entry.data)->Message.decode(PluginSpec.eventSchema)
|
|
31
|
+
|
|
32
|
+
describe("The stored plugin-lifecycle payload corpus", () => {
|
|
33
|
+
testSync("is not empty (a corpus that loaded nothing would pass vacuously)", () =>
|
|
34
|
+
expect(entries->Array.length > 0)->toBe(true)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
entries->Array.forEach(entry =>
|
|
38
|
+
testSync(`still decodes: ${entry.name}`, () => {
|
|
39
|
+
// Sury's own exception carries the field path, which is the whole value of
|
|
40
|
+
// this suite's failure message — report it verbatim rather than "it threw".
|
|
41
|
+
let outcome = switch decodeStored(entry) {
|
|
42
|
+
| _ => "decodes"
|
|
43
|
+
| exception S.Error(err) => err.message
|
|
44
|
+
| exception JsExn(e) => `threw: ${JsExn.message(e)->Option.getOr("no message")}`
|
|
45
|
+
| exception _ => "threw a non-JS exception"
|
|
46
|
+
}
|
|
47
|
+
expect(outcome)->toBe("decodes")
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
7
|
+
import * as Message$ReventlessCore from "../../src/Message.res.mjs";
|
|
8
|
+
import * as PluginSpec$ReventlessCore from "../../src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
9
|
+
import * as PluginLifecycleCorpus$ReventlessCore from "./PluginLifecycleCorpus.res.mjs";
|
|
10
|
+
|
|
11
|
+
function decodeStored(entry) {
|
|
12
|
+
return Message$ReventlessCore.decode(Message$ReventlessCore.combineMessage(entry.event, entry.data), PluginSpec$ReventlessCore.eventSchema);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
globalThis.describe("The stored plugin-lifecycle payload corpus", () => {
|
|
16
|
+
globalThis.test("is not empty (a corpus that loaded nothing would pass vacuously)", () => {
|
|
17
|
+
globalThis.expect(PluginLifecycleCorpus$ReventlessCore.entries.length !== 0).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
PluginLifecycleCorpus$ReventlessCore.entries.forEach(entry => {
|
|
20
|
+
globalThis.test(`still decodes: ` + entry.name, () => {
|
|
21
|
+
let outcome;
|
|
22
|
+
try {
|
|
23
|
+
decodeStored(entry);
|
|
24
|
+
outcome = "decodes";
|
|
25
|
+
} catch (raw_err) {
|
|
26
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
27
|
+
outcome = err.RE_EXN_ID === S.$$Error ? err._1.message : (
|
|
28
|
+
err.RE_EXN_ID === "JsExn" ? `threw: ` + Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "no message") : "threw a non-JS exception"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
globalThis.expect(outcome).toBe("decodes");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let entries = PluginLifecycleCorpus$ReventlessCore.entries;
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
entries,
|
|
40
|
+
decodeStored,
|
|
41
|
+
}
|
|
42
|
+
/* Not a pure module */
|
|
@@ -718,13 +718,13 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
718
718
|
store: "TestPlugin.documents",
|
|
719
719
|
component: "AttachInvoice",
|
|
720
720
|
field: "documentUrl",
|
|
721
|
-
annotation: "documents",
|
|
721
|
+
annotation: Some("documents"),
|
|
722
722
|
},
|
|
723
723
|
{
|
|
724
724
|
store: "branding.logos",
|
|
725
725
|
component: "AttachInvoice",
|
|
726
726
|
field: "logoUrl",
|
|
727
|
-
annotation: "branding.logos",
|
|
727
|
+
annotation: Some("branding.logos"),
|
|
728
728
|
},
|
|
729
729
|
]
|
|
730
730
|
expect(withStores.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
@@ -815,7 +815,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
815
815
|
store: "TestPlugin.avatars",
|
|
816
816
|
component: "UploadAvatar",
|
|
817
817
|
field: "avatarUrl",
|
|
818
|
-
annotation: "avatars",
|
|
818
|
+
annotation: Some("avatars"),
|
|
819
819
|
},
|
|
820
820
|
]
|
|
821
821
|
expect(withOptional.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Every bare required scalar reachable from `pluginDefinition`, sorted.
|
|
2
|
+
#
|
|
3
|
+
# This file is a tripwire, not an inventory to keep tidy. Everything reachable from
|
|
4
|
+
# `pluginDefinition` is persisted in the Plugin lifecycle aggregate's event log, and a
|
|
5
|
+
# scalar is the one shape `Message.fillMissingDefaults` cannot derive a value for: adding
|
|
6
|
+
# one means every message stored before it decodes with a value this framework invented
|
|
7
|
+
# (`""` / `0` / `false`), reported at runtime as a warning and nowhere else.
|
|
8
|
+
#
|
|
9
|
+
# So a diff here is a design decision, not a chore. If your new field can be absent, give
|
|
10
|
+
# it the `js_nullable` (`T | null`) shape and this list does not change. If it genuinely
|
|
11
|
+
# must be a required scalar, add the line and accept that older payloads carry a fabricated
|
|
12
|
+
# value for it.
|
|
13
|
+
#
|
|
14
|
+
# Regenerate ONLY after making that call — never to make a red build green.
|
|
15
|
+
# See docs/analysis/plugin-definition-schema-evolution-wedge.md.
|
|
16
|
+
#
|
|
17
|
+
# A `js_nullable` (`T | null`) scalar is NOT listed: absent heals to null, which is the
|
|
18
|
+
# shape the rule asks for. A scalar nested inside a nullable object or array IS listed —
|
|
19
|
+
# when an old payload carries the parent, the new field still has to be invented. That is
|
|
20
|
+
# how requiredStoreDeclarations[].annotation, a required string inside a nullable array,
|
|
21
|
+
# froze a plugin's registration for two days; it is absent from this list because it has
|
|
22
|
+
# since been made `js_nullable`.
|
|
23
|
+
.apiSchemaFragment.encoded: string
|
|
24
|
+
.apiSchemaFragment.protocol: string
|
|
25
|
+
.dcbEventLog.eventTopicArn: string
|
|
26
|
+
.dcbEventLog.name: string
|
|
27
|
+
.eventCollector: string
|
|
28
|
+
.extensionPoints[].commandTopic: string
|
|
29
|
+
.extensionPoints[].eventTopic: string
|
|
30
|
+
.extensionPoints[].name: string
|
|
31
|
+
.extensionProtocols[].commandVersion: string
|
|
32
|
+
.extensionProtocols[].eventVersion: string
|
|
33
|
+
.extensionProtocols[].extensionPointName: string
|
|
34
|
+
.extensions[].dcbSources[]: string
|
|
35
|
+
.extensions[].extensionPointName: string
|
|
36
|
+
.extensions[].name: string
|
|
37
|
+
.id: string
|
|
38
|
+
.name: string
|
|
39
|
+
.structure.aggregates[].commands[].allowedStates[]: string
|
|
40
|
+
.structure.aggregates[].commands[].mutationField: string
|
|
41
|
+
.structure.aggregates[].commands[].name: string
|
|
42
|
+
.structure.aggregates[].commands[].references[].entity: string
|
|
43
|
+
.structure.aggregates[].commands[].references[].fieldName: string
|
|
44
|
+
.structure.aggregates[].commands[].schema: string
|
|
45
|
+
.structure.aggregates[].consumedEventTypes[]: string
|
|
46
|
+
.structure.aggregates[].events[].name: string
|
|
47
|
+
.structure.aggregates[].events[].references[].entity: string
|
|
48
|
+
.structure.aggregates[].events[].references[].fieldName: string
|
|
49
|
+
.structure.aggregates[].events[].schema: string
|
|
50
|
+
.structure.aggregates[].linkedViews[]: string
|
|
51
|
+
.structure.aggregates[].name: string
|
|
52
|
+
.structure.aggregates[].producedEventTypes[]: string
|
|
53
|
+
.structure.automationSlices[].consumedEventTypes[]: string
|
|
54
|
+
.structure.automationSlices[].name: string
|
|
55
|
+
.structure.automationSlices[].producedCommandTypes[]: string
|
|
56
|
+
.structure.automationSlices[].targetName: string
|
|
57
|
+
.structure.extensionPoints[].commandTypes[]: string
|
|
58
|
+
.structure.extensionPoints[].delegateNames[]: string
|
|
59
|
+
.structure.extensionPoints[].name: string
|
|
60
|
+
.structure.extensionPoints[].sourceEventTypes[]: string
|
|
61
|
+
.structure.extensions[].commandTypes[]: string
|
|
62
|
+
.structure.extensions[].delegateNames[]: string
|
|
63
|
+
.structure.extensions[].eventTypes[]: string
|
|
64
|
+
.structure.extensions[].name: string
|
|
65
|
+
.structure.inboundTranslationSlices[].commandTypes[]: string
|
|
66
|
+
.structure.inboundTranslationSlices[].name: string
|
|
67
|
+
.structure.inboundTranslationSlices[].targetName: string
|
|
68
|
+
.structure.outboundTranslationSlices[].consumedEventTypes[]: string
|
|
69
|
+
.structure.outboundTranslationSlices[].inboundCommandTypes[]: string
|
|
70
|
+
.structure.outboundTranslationSlices[].name: string
|
|
71
|
+
.structure.readModels[].consumedEventTypes[]: string
|
|
72
|
+
.structure.readModels[].labelField: string
|
|
73
|
+
.structure.readModels[].linkedWriteSide[]: string
|
|
74
|
+
.structure.readModels[].name: string
|
|
75
|
+
.structure.readModels[].queryField: string
|
|
76
|
+
.structure.readModels[].schema: string
|
|
77
|
+
.structure.readModels[].searchableFields[]: string
|
|
78
|
+
.structure.requiredStoreDeclarations[].component: string
|
|
79
|
+
.structure.requiredStoreDeclarations[].field: string
|
|
80
|
+
.structure.requiredStoreDeclarations[].store: string
|
|
81
|
+
.structure.requiredStores[]: string
|
|
82
|
+
.structure.stateChangeSlices[].commands[].allowedStates[]: string
|
|
83
|
+
.structure.stateChangeSlices[].commands[].mutationField: string
|
|
84
|
+
.structure.stateChangeSlices[].commands[].name: string
|
|
85
|
+
.structure.stateChangeSlices[].commands[].references[].entity: string
|
|
86
|
+
.structure.stateChangeSlices[].commands[].references[].fieldName: string
|
|
87
|
+
.structure.stateChangeSlices[].commands[].schema: string
|
|
88
|
+
.structure.stateChangeSlices[].consumedEventTypes[]: string
|
|
89
|
+
.structure.stateChangeSlices[].events[].name: string
|
|
90
|
+
.structure.stateChangeSlices[].events[].references[].entity: string
|
|
91
|
+
.structure.stateChangeSlices[].events[].references[].fieldName: string
|
|
92
|
+
.structure.stateChangeSlices[].events[].schema: string
|
|
93
|
+
.structure.stateChangeSlices[].linkedViews[]: string
|
|
94
|
+
.structure.stateChangeSlices[].name: string
|
|
95
|
+
.structure.stateChangeSlices[].producedEventTypes[]: string
|
|
96
|
+
.structure.stateViewSlices[].consumedEventTypes[]: string
|
|
97
|
+
.structure.stateViewSlices[].labelField: string
|
|
98
|
+
.structure.stateViewSlices[].linkedWriteSide[]: string
|
|
99
|
+
.structure.stateViewSlices[].name: string
|
|
100
|
+
.structure.stateViewSlices[].queryField: string
|
|
101
|
+
.structure.stateViewSlices[].schema: string
|
|
102
|
+
.structure.stateViewSlices[].searchableFields[]: string
|
|
103
|
+
.version: string
|