@reventlessdev/reventless-local 3.0.0-alpha.290 → 3.0.0-alpha.292
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 +21 -0
- package/package.json +11 -11
- package/src/BakedManifest.res +7 -2
- package/src/BakedManifest.res.mjs +4 -4
- package/src/HostShellDist.res +11 -0
- package/src/HostShellDist.res.mjs +14 -1
- package/src/Platform.res +31 -19
- package/src/Platform.res.mjs +63 -60
- package/src/ShellConfig.res +27 -24
- package/src/ShellConfig.res.mjs +25 -13
- package/src/UiHints.res +4 -6
- package/src/UiSlots.res +2 -3
- package/src/adapter/Auth/LocalAuth.res +2 -2
- package/src/adapter/Backend.res +2 -2
- package/src/adapter/LocalBus.res +1 -1
- package/src/adapter/LocalUploadResolvers.res +1 -1
- package/src/components/Aggregate_Builder.res +2 -2
- package/src/components/Aggregate_Builder.res.mjs +4 -4
- package/src/components/DcbEventLog_Builder.res +1 -1
- package/src/components/StateChangeSlice_Builder.res +3 -3
- package/src/components/StateChangeSlice_Builder.res.mjs +3 -3
- package/src/reset/LocalSeedReset.res +1 -1
- package/tests/HostShellDistTest.res +70 -0
- package/tests/HostShellDistTest.res.mjs +75 -0
- package/tests/ShellConfigTest.res +45 -7
- package/tests/ShellConfigTest.res.mjs +45 -8
- package/tests/adapter/BackendParityTest.res +1 -1
- package/tests/adapter/EventHistoryResolverTest.res +2 -2
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +1 -1
- package/tests/adapter/LocalAuthLoginTest.res +1 -1
- package/tests/adapter/QueryDbListResolverTest.res +1 -1
- package/tests/adapter/QueryDbResolvedFieldsTest.res +1 -1
- package/tests/components/aggregate/AggregateFixtures.res +6 -6
- package/tests/components/aggregate/AggregateFixtures.res.mjs +3 -3
- package/tests/components/aggregate/AggregateSelfMappingFixtures.res +2 -2
- package/tests/components/aggregate/AggregateSelfMappingFixtures.res.mjs +3 -3
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +4 -4
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +4 -4
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res +3 -3
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res.mjs +4 -4
- package/tests/components/dcb/DcbCrossPartitionFixtures.res +2 -2
- package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +2 -2
- package/tests/components/dcb/DcbFixtures.res +2 -2
- package/tests/components/dcb/DcbFixtures.res.mjs +2 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +1 -1
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +2 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +2 -2
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +2 -2
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res.mjs +2 -2
- package/tests/plugin/{PluginBehavior_GWT.res → PluginDecision_GWT.res} +2 -2
- package/tests/plugin/{PluginBehavior_GWT.res.mjs → PluginDecision_GWT.res.mjs} +8 -8
- package/tests/plugin/UiFragmentRegistry_GWT.res +1 -1
- package/tests/plugin/UiFragmentRegistry_GWT.res.mjs +5 -5
package/src/ShellConfig.res
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
// `pnpm install` that replaces the package directory costs one restart, because
|
|
18
18
|
// the baseline is re-seeded from whatever the new package ships.
|
|
19
19
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
20
|
+
// The computed keys are the environment (always `local`), `manifestUrl` and the
|
|
21
|
+
// journey map (from the bake declaration — local knows where it put the file),
|
|
22
|
+
// `uiSlotsUrl` and `adminGroup`; the rest are the deployment's own `shellConfig`,
|
|
22
23
|
// passed through verbatim. A `shellConfig` that names a computed key fails the
|
|
23
24
|
// same way it does on AWS: a passthrough cannot redirect a key the platform
|
|
24
25
|
// decides, and silently resolving it either way points the shell somewhere
|
|
@@ -41,7 +42,10 @@ let manifestUrlOf = (config: ReventlessInfra.Platform.bakedManifest): string =>
|
|
|
41
42
|
// caller matching no declared group gets and what every existing deployment has.
|
|
42
43
|
let journeyManifestsKey = "journeyManifestUrls"
|
|
43
44
|
|
|
45
|
+
let environmentKeys = [Reventless.ShellEnvironment.nameKey, Reventless.ShellEnvironment.purposeKey]
|
|
46
|
+
|
|
44
47
|
let computedKeys = [
|
|
48
|
+
...environmentKeys,
|
|
45
49
|
"manifestUrl",
|
|
46
50
|
journeyManifestsKey,
|
|
47
51
|
ReventlessCore.Platform_UiSlots.configKey,
|
|
@@ -60,7 +64,10 @@ let overlay = (
|
|
|
60
64
|
~uiSlotsFile: option<string>=?,
|
|
61
65
|
~shellConfig: option<dict<JSON.t>>,
|
|
62
66
|
): dict<JSON.t> => {
|
|
63
|
-
let out =
|
|
67
|
+
let out =
|
|
68
|
+
Reventless.ShellEnvironment.local
|
|
69
|
+
->Reventless.ShellEnvironment.configFields
|
|
70
|
+
->Dict.fromArray
|
|
64
71
|
|
|
65
72
|
// `UiSlots` serves the module; this is what tells the shell to import it. The
|
|
66
73
|
// two are separate writes to separate files, which is exactly why the name
|
|
@@ -120,23 +127,22 @@ let readObject = (~path: string, ~label: string): dict<JSON.t> =>
|
|
|
120
127
|
/**
|
|
121
128
|
Merge the overlay onto the shipped `config.json` in the host shell's `dist/`.
|
|
122
129
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
The environment keys are always written, so the shell can say it is local;
|
|
131
|
+
without the package they are dropped silently, since a platform nobody points a
|
|
132
|
+
browser at has nothing to badge. Anything declared beyond them is written or
|
|
133
|
+
fails loudly, for the reason `BakedManifest.emit` gives: both failure modes it
|
|
134
|
+
can hit are the deployment's own mistake, and both produce the same silent
|
|
135
|
+
symptom if swallowed.
|
|
129
136
|
*/
|
|
130
137
|
let emit = (
|
|
131
138
|
~bakedManifest: option<ReventlessInfra.Platform.bakedManifest>,
|
|
132
139
|
~uiSlotsFile: option<string>=?,
|
|
133
140
|
~shellConfig: option<dict<JSON.t>>,
|
|
134
|
-
//
|
|
135
|
-
// it — "boot twice and the second write still starts from the shipped file" is
|
|
136
|
-
// not a property a pure function can carry.
|
|
141
|
+
// The platform's chosen `assetsDir`; unset, the host-shell package's `dist/`.
|
|
137
142
|
~dir: option<string>=?,
|
|
138
143
|
) => {
|
|
139
144
|
let overlay = overlay(~bakedManifest, ~uiSlotsFile?, ~shellConfig)
|
|
145
|
+
let declared = overlay->Dict.keysToArray->Array.filter(k => !(environmentKeys->Array.includes(k)))
|
|
140
146
|
|
|
141
147
|
switch switch dir {
|
|
142
148
|
| Some(_) as given => given
|
|
@@ -146,7 +152,7 @@ let emit = (
|
|
|
146
152
|
// Nothing declared and no shell installed is the ordinary case for a
|
|
147
153
|
// platform nobody points a browser at; only a declaration makes the missing
|
|
148
154
|
// package an error, and then it is the same error the bake gives.
|
|
149
|
-
if
|
|
155
|
+
if declared->Array.length > 0 {
|
|
150
156
|
JsError.throwWithMessage(
|
|
151
157
|
`host UI config.json: cannot resolve ${HostShellDist.package} from ${NodeProcess.cwd()} — ` ++
|
|
152
158
|
`the local shell reads its config from that package's dist/, so declaring shell ` ++
|
|
@@ -157,29 +163,26 @@ let emit = (
|
|
|
157
163
|
let path = NodePath.join([dir, fileName])
|
|
158
164
|
let baselinePath = NodePath.join([dir, baselineFileName])
|
|
159
165
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
// The baseline is whatever `config.json` the directory held first: the
|
|
167
|
+
// shell's shipped file, or the app's own in a chosen `assetsDir`.
|
|
168
|
+
if !NodeFs.existsSync(baselinePath) {
|
|
169
|
+
if NodeFs.existsSync(path) {
|
|
170
|
+
NodeFs.writeFileSync(baselinePath, NodeFs.readFileSync(path))
|
|
171
|
+
} else if declared->Array.length > 0 {
|
|
165
172
|
JsError.throwWithMessage(
|
|
166
|
-
`host UI config.json:
|
|
173
|
+
`host UI config.json: there is no ${fileName} at ${dir} — ` ++
|
|
167
174
|
`there is no baseline to overlay, so the shell would boot with only the keys ` ++ `declared here and none of the ones it expects.`,
|
|
168
175
|
)
|
|
169
176
|
}
|
|
170
|
-
NodeFs.writeFileSync(baselinePath, NodeFs.readFileSync(path))
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
if NodeFs.existsSync(baselinePath) {
|
|
174
180
|
let merged = readObject(~path=baselinePath, ~label="config baseline")
|
|
175
181
|
overlay->Dict.forEachWithKey((v, k) => merged->Dict.set(k, v))
|
|
176
182
|
NodeFs.writeFileSync(path, JSON.stringify(merged->JSON.Encode.object, ~space=2))
|
|
177
|
-
let keys = overlay->Dict.keysToArray
|
|
178
183
|
log.info(
|
|
179
184
|
~comp="ShellConfig",
|
|
180
|
-
|
|
181
|
-
? `restored ${fileName} from ${baselineFileName}: ${path}`
|
|
182
|
-
: `wrote ${fileName} with ${keys->Array.join(", ")}: ${path}`,
|
|
185
|
+
`wrote ${fileName} with ${overlay->Dict.keysToArray->Array.join(", ")}: ${path}`,
|
|
183
186
|
)
|
|
184
187
|
}
|
|
185
188
|
}
|
package/src/ShellConfig.res.mjs
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Nodefs from "node:fs";
|
|
4
4
|
import * as Nodepath from "node:path";
|
|
5
|
+
import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
|
|
5
6
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
6
7
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
7
8
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
9
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
10
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
11
|
+
import * as ShellEnvironment$Reventless from "@reventlessdev/reventless-spec/src/types/ShellEnvironment.res.mjs";
|
|
10
12
|
import * as HostShellDist$ReventlessLocal from "./HostShellDist.res.mjs";
|
|
11
13
|
import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
|
|
12
14
|
import * as Platform_AdminGroup$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminGroup.res.mjs";
|
|
@@ -24,15 +26,23 @@ function manifestUrlOf(config) {
|
|
|
24
26
|
|
|
25
27
|
let journeyManifestsKey = "journeyManifestUrls";
|
|
26
28
|
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Platform_UiSlots$ReventlessCore.configKey,
|
|
31
|
-
Platform_AdminGroup$ReventlessCore.configKey
|
|
29
|
+
let environmentKeys = [
|
|
30
|
+
ShellEnvironment$Reventless.nameKey,
|
|
31
|
+
ShellEnvironment$Reventless.purposeKey
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
+
let computedKeys = Belt_Array.concatMany([
|
|
35
|
+
environmentKeys,
|
|
36
|
+
[
|
|
37
|
+
"manifestUrl",
|
|
38
|
+
journeyManifestsKey,
|
|
39
|
+
Platform_UiSlots$ReventlessCore.configKey,
|
|
40
|
+
Platform_AdminGroup$ReventlessCore.configKey
|
|
41
|
+
]
|
|
42
|
+
]);
|
|
43
|
+
|
|
34
44
|
function overlay(bakedManifest, uiSlotsFile, shellConfig) {
|
|
35
|
-
let out =
|
|
45
|
+
let out = Object.fromEntries(ShellEnvironment$Reventless.configFields(ShellEnvironment$Reventless.local));
|
|
36
46
|
Stdlib_Option.forEach(uiSlotsFile, param => {
|
|
37
47
|
out[Platform_UiSlots$ReventlessCore.configKey] = Platform_UiSlots$ReventlessCore.url;
|
|
38
48
|
});
|
|
@@ -78,9 +88,10 @@ function readObject(path, label) {
|
|
|
78
88
|
|
|
79
89
|
function emit(bakedManifest, uiSlotsFile, shellConfig, dir) {
|
|
80
90
|
let overlay$1 = overlay(bakedManifest, uiSlotsFile, shellConfig);
|
|
91
|
+
let declared = Object.keys(overlay$1).filter(k => !environmentKeys.includes(k));
|
|
81
92
|
let dir$1 = dir !== undefined ? dir : HostShellDist$ReventlessLocal.dir();
|
|
82
93
|
if (dir$1 === undefined) {
|
|
83
|
-
if (
|
|
94
|
+
if (declared.length !== 0) {
|
|
84
95
|
return Stdlib_JsError.throwWithMessage(`host UI config.json: cannot resolve ` + HostShellDist$ReventlessLocal.$$package + ` from ` + process.cwd() + ` — the local shell reads its config from that package's dist/, so declaring shell config without the package installed would write nothing and leave the shell configured as it shipped.`);
|
|
85
96
|
} else {
|
|
86
97
|
return;
|
|
@@ -88,11 +99,12 @@ function emit(bakedManifest, uiSlotsFile, shellConfig, dir) {
|
|
|
88
99
|
}
|
|
89
100
|
let path = Nodepath.join(dir$1, fileName);
|
|
90
101
|
let baselinePath = Nodepath.join(dir$1, baselineFileName);
|
|
91
|
-
if (!Nodefs.existsSync(baselinePath)
|
|
92
|
-
if (
|
|
93
|
-
|
|
102
|
+
if (!Nodefs.existsSync(baselinePath)) {
|
|
103
|
+
if (Nodefs.existsSync(path)) {
|
|
104
|
+
Nodefs.writeFileSync(baselinePath, Nodefs.readFileSync(path, "utf8"), "utf8");
|
|
105
|
+
} else if (declared.length !== 0) {
|
|
106
|
+
Stdlib_JsError.throwWithMessage(`host UI config.json: there is no ` + fileName + ` at ` + dir$1 + ` — there is no baseline to overlay, so the shell would boot with only the keys declared here and none of the ones it expects.`);
|
|
94
107
|
}
|
|
95
|
-
Nodefs.writeFileSync(baselinePath, Nodefs.readFileSync(path, "utf8"), "utf8");
|
|
96
108
|
}
|
|
97
109
|
if (!Nodefs.existsSync(baselinePath)) {
|
|
98
110
|
return;
|
|
@@ -102,8 +114,7 @@ function emit(bakedManifest, uiSlotsFile, shellConfig, dir) {
|
|
|
102
114
|
merged[k] = v;
|
|
103
115
|
});
|
|
104
116
|
Nodefs.writeFileSync(path, JSON.stringify(merged, undefined, 2), "utf8");
|
|
105
|
-
|
|
106
|
-
log.info("ShellConfig", undefined, keys.length === 0 ? `restored ` + fileName + ` from ` + baselineFileName + `: ` + path : `wrote ` + fileName + ` with ` + keys.join(", ") + `: ` + path);
|
|
117
|
+
log.info("ShellConfig", undefined, `wrote ` + fileName + ` with ` + Object.keys(overlay$1).join(", ") + `: ` + path);
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
export {
|
|
@@ -112,6 +123,7 @@ export {
|
|
|
112
123
|
baselineFileName,
|
|
113
124
|
manifestUrlOf,
|
|
114
125
|
journeyManifestsKey,
|
|
126
|
+
environmentKeys,
|
|
115
127
|
computedKeys,
|
|
116
128
|
overlay,
|
|
117
129
|
readObject,
|
package/src/UiHints.res
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// withdrawn declaration leaves yesterday's hints in place with nothing in the
|
|
13
13
|
// diff to explain them. Unlike `ShellConfig` there is no merge — AWS writes the
|
|
14
14
|
// declared file verbatim, and layering a deployment's hints over the shell
|
|
15
|
-
// package's demonstration hints would invent a third
|
|
15
|
+
// package's demonstration hints would invent a third behavior neither platform
|
|
16
16
|
// has.
|
|
17
17
|
|
|
18
18
|
let log = ReventlessCore.Logger.fromEnv()
|
|
@@ -37,9 +37,8 @@ let baselineFileName = "ui-hints.base.json"
|
|
|
37
37
|
*/
|
|
38
38
|
let emit = (
|
|
39
39
|
~uiHintsFile: option<string>,
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// shipped file" is not a property a pure function can carry.
|
|
40
|
+
// Where the file is served from: the platform's chosen `assetsDir`; unset, the
|
|
41
|
+
// host-shell package's `dist/`.
|
|
43
42
|
~dir: option<string>=?,
|
|
44
43
|
) => {
|
|
45
44
|
// Read before anything is touched, so a bad declaration cannot leave the
|
|
@@ -125,8 +124,7 @@ let emit = (
|
|
|
125
124
|
*/
|
|
126
125
|
let watch = (
|
|
127
126
|
~uiHintsFile: option<string>,
|
|
128
|
-
// The
|
|
129
|
-
// watch a file it wrote into a temp dir without a host-shell package on disk.
|
|
127
|
+
// The directory `emit` takes, threaded to it.
|
|
130
128
|
~dir: option<string>=?,
|
|
131
129
|
~onReload: unit => unit,
|
|
132
130
|
): option<NodeFs.watcher> =>
|
package/src/UiSlots.res
CHANGED
|
@@ -53,9 +53,8 @@ let fileName = ReventlessCore.Platform_UiSlots.fileName
|
|
|
53
53
|
*/
|
|
54
54
|
let emit = (
|
|
55
55
|
~uiSlotsFile: option<string>,
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// property a pure function can carry.
|
|
56
|
+
// Where the module is served from: the platform's chosen `assetsDir`; unset,
|
|
57
|
+
// the host-shell package's `dist/`.
|
|
59
58
|
~dir: option<string>=?,
|
|
60
59
|
) => {
|
|
61
60
|
// Read before anything is touched, so a bad declaration cannot leave the
|
|
@@ -113,7 +113,7 @@ module Login = {
|
|
|
113
113
|
// every ReScript rebuild, so a per-process secret logs the developer out
|
|
114
114
|
// several times an hour, mid-task, with a "reconnecting" spinner as the
|
|
115
115
|
// only explanation.
|
|
116
|
-
// 3. A random 32 bytes, per process — today's
|
|
116
|
+
// 3. A random 32 bytes, per process — today's behavior, kept for the run
|
|
117
117
|
// that has no `.reventless/` to write to.
|
|
118
118
|
//
|
|
119
119
|
// The directory is used but never created. Its presence is what distinguishes
|
|
@@ -389,7 +389,7 @@ let authenticate = async (ctx: ReventlessCore.Auth_Adapter.requestContext): Iden
|
|
|
389
389
|
| None =>
|
|
390
390
|
// No headers: in-memory mode defaults to defaultUser so local dev
|
|
391
391
|
// against an AllowAuthenticated-defaulted backend "just works" without
|
|
392
|
-
// setting headers. Anonymous
|
|
392
|
+
// setting headers. Anonymous behavior is reachable only by building
|
|
393
393
|
// an Identity.anonymous explicitly in code.
|
|
394
394
|
Authenticated(defaultUser)
|
|
395
395
|
| Some(g) =>
|
package/src/adapter/Backend.res
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Selects which storage backend the in-memory Platform uses.
|
|
2
2
|
//
|
|
3
|
-
// Memory — pure in-memory dicts/refs, current default
|
|
3
|
+
// Memory — pure in-memory dicts/refs, current default behavior, wiped on restart.
|
|
4
4
|
// Sqlite — opt-in on-disk SQLite file via SqliteDriver. Survives process restart.
|
|
5
5
|
// resetOnStart=true wipes the file when the Platform is constructed.
|
|
6
6
|
|
|
@@ -41,7 +41,7 @@ let removeFileIfExists = (path: string) =>
|
|
|
41
41
|
| _ => ()
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// Parses REVENTLESS_LOCAL_BACKEND.
|
|
44
|
+
// Parses REVENTLESS_LOCAL_BACKEND. Recognized forms:
|
|
45
45
|
// sqlite:./local.db — Sqlite backend, no reset on start
|
|
46
46
|
// sqlite:./local.db?reset — Sqlite backend, wipes file at Platform.Make time
|
|
47
47
|
// memory — explicit Memory selector
|
package/src/adapter/LocalBus.res
CHANGED
|
@@ -117,7 +117,7 @@ module type T = {
|
|
|
117
117
|
let registerQueryDbScan: (string, unit => array<JSON.t>) => unit
|
|
118
118
|
let getQueryDbScan: string => option<unit => array<JSON.t>>
|
|
119
119
|
// Stream variant: lazily creates a Stream from current storage contents.
|
|
120
|
-
// Used by LocalQueryEngine to
|
|
120
|
+
// Used by LocalQueryEngine to honor ~limit via Stream.take without loading all items.
|
|
121
121
|
let registerQueryDbStream: (string, unit => Stream.t<JSON.t, string, unit>) => unit
|
|
122
122
|
let getQueryDbStream: string => option<unit => Stream.t<JSON.t, string, unit>>
|
|
123
123
|
// Indexed equality lookup: (field, value) → matching items. The SQLite backend
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Local upload resolver logic (route B), backed by `LocalObjectStore`. Shared by the
|
|
2
2
|
// dev platform (`Platform.res` registers these on the domain server) and
|
|
3
|
-
// `ServedBucketHttpTest`, so the mint/release
|
|
3
|
+
// `ServedBucketHttpTest`, so the mint/release behavior has one definition.
|
|
4
4
|
//
|
|
5
5
|
// The dev store has no identities and no clock, so release enforces only the *shape*
|
|
6
6
|
// of the rule (key under a served prefix), not the identity/age conditions AWS applies
|
|
@@ -28,12 +28,12 @@ module MakeWithHooks = (
|
|
|
28
28
|
|
|
29
29
|
module Make = (
|
|
30
30
|
Spec: Reventless.Aggregate.Spec,
|
|
31
|
-
|
|
31
|
+
Decision: Reventless.Decision.T with module Spec := Spec,
|
|
32
32
|
EventMappings: ReventlessInfra.EventMapper.Mappings with module Target := Spec,
|
|
33
33
|
) => {
|
|
34
34
|
include ReventlessCore.Aggregate_Builder.Make(
|
|
35
35
|
Spec,
|
|
36
|
-
|
|
36
|
+
Decision,
|
|
37
37
|
EventMappings,
|
|
38
38
|
RuntimeEnvironment,
|
|
39
39
|
CommandGeneratorResolvers_GraphQL,
|
|
@@ -56,8 +56,8 @@ function MakeWithHooks(Bus) {
|
|
|
56
56
|
let AggregateRuntimeBuilder = LocalAggregateRuntime_Builder$ReventlessLocal.Make(Bus)({
|
|
57
57
|
make: CommandTopicChannel.make
|
|
58
58
|
})(EventCollectorChannel);
|
|
59
|
-
let Make = Spec => (
|
|
60
|
-
let include = Aggregate_Builder$ReventlessCore.Make(Spec)(
|
|
59
|
+
let Make = Spec => (Decision => (EventMappings => {
|
|
60
|
+
let include = Aggregate_Builder$ReventlessCore.Make(Spec)(Decision)({
|
|
61
61
|
mappings: EventMappings.mappings,
|
|
62
62
|
counter: EventMappings.counter
|
|
63
63
|
})({
|
|
@@ -105,8 +105,8 @@ function Make(Bus) {
|
|
|
105
105
|
let AggregateRuntimeBuilder = LocalAggregateRuntime_Builder$ReventlessLocal.Make(Bus)({
|
|
106
106
|
make: CommandTopicChannel.make
|
|
107
107
|
})(EventCollectorChannel);
|
|
108
|
-
let Make$1 = Spec => (
|
|
109
|
-
let include = Aggregate_Builder$ReventlessCore.Make(Spec)(
|
|
108
|
+
let Make$1 = Spec => (Decision => (EventMappings => {
|
|
109
|
+
let include = Aggregate_Builder$ReventlessCore.Make(Spec)(Decision)({
|
|
110
110
|
mappings: EventMappings.mappings,
|
|
111
111
|
counter: EventMappings.counter
|
|
112
112
|
})({
|
|
@@ -4,7 +4,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
4
4
|
module EventTopicPublisher = LocalEventTopicPublisher.Make(Bus)
|
|
5
5
|
|
|
6
6
|
// Use the backend-aware storage functor (as Platform does), not the plain
|
|
7
|
-
// module: the functor consults BackendState so a standalone DCB log
|
|
7
|
+
// module: the functor consults BackendState so a standalone DCB log honors
|
|
8
8
|
// REVENTLESS_LOCAL_BACKEND=sqlite, and it registers the read with the Bus so
|
|
9
9
|
// `Bus.getDcbEventLogRead` can see it. The plain module did neither — the log
|
|
10
10
|
// silently stayed in memory under SQLite and was invisible to bus readers.
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Make = (
|
|
4
4
|
Spec: Reventless.StateChangeSlice.Spec,
|
|
5
|
-
|
|
5
|
+
Decision: Reventless.StateChangeSlice.Decision with module Spec := Spec,
|
|
6
6
|
) => {
|
|
7
|
-
module Inner = ReventlessCore.StateChangeSlice_Builder.Make(Spec,
|
|
7
|
+
module Inner = ReventlessCore.StateChangeSlice_Builder.Make(Spec, Decision)
|
|
8
8
|
module Spec = Spec
|
|
9
|
-
module
|
|
9
|
+
module Decision = Decision
|
|
10
10
|
let isAsync = Inner.isAsync
|
|
11
11
|
type component = Inner.component
|
|
12
12
|
let make: (
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
import * as StateChangeSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/StateChangeSlice/StateChangeSlice_Builder.res.mjs";
|
|
4
4
|
|
|
5
5
|
function Make(Spec) {
|
|
6
|
-
return
|
|
7
|
-
let Inner = StateChangeSlice_Builder$ReventlessCore.Make(Spec)(
|
|
6
|
+
return Decision => {
|
|
7
|
+
let Inner = StateChangeSlice_Builder$ReventlessCore.Make(Spec)(Decision);
|
|
8
8
|
return {
|
|
9
9
|
Inner: Inner,
|
|
10
10
|
Spec: Spec,
|
|
11
|
-
|
|
11
|
+
Decision: Decision,
|
|
12
12
|
isAsync: Inner.isAsync,
|
|
13
13
|
make: Inner.make
|
|
14
14
|
};
|
|
@@ -552,7 +552,7 @@ let run = (~dbPath: option<string>=?): unit => {
|
|
|
552
552
|
Console.log("Nothing to do.")
|
|
553
553
|
} else {
|
|
554
554
|
// Set at all ⇒ it is the answer, affirmative or not. Falling through to
|
|
555
|
-
// the prompt on an
|
|
555
|
+
// the prompt on an unrecognized value would ask for a TTY the runs this
|
|
556
556
|
// variable exists for do not have, so `SEED_RESET_CONFIRM=no` would
|
|
557
557
|
// throw where it plainly means "don't". Both arms read the reply
|
|
558
558
|
// through one predicate, which is what keeps the typed `y` and the
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The one directory every local host-UI writer writes into. An app that composes
|
|
4
|
+
// its own UI bundle names it as `assetsDir`, as on AWS; a writer that took a
|
|
5
|
+
// second opinion would put its file where nothing serves it.
|
|
6
|
+
|
|
7
|
+
let _ = TestRunner.setup()
|
|
8
|
+
|
|
9
|
+
let tmpdir = prefix => NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), prefix]))
|
|
10
|
+
|
|
11
|
+
// What the package's dist/ holds, so a test can say it was not touched. Empty
|
|
12
|
+
// when the package is not installed where the test runs.
|
|
13
|
+
let snapshot = () =>
|
|
14
|
+
HostShellDist.dir()
|
|
15
|
+
->Option.filter(NodeFs.existsSync)
|
|
16
|
+
->Option.mapOr([], dir =>
|
|
17
|
+
NodeFs.readdirSyncNames(dir, {})
|
|
18
|
+
->Array.toSorted(String.compare)
|
|
19
|
+
->Array.map(name => {
|
|
20
|
+
let path = NodePath.join([dir, name])
|
|
21
|
+
(name, NodeFs.statSync(path)->NodeFs.mtimeMs)
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
describe("HostShellDist.resolve", () => {
|
|
26
|
+
testSync("is the package's dist/ when no directory is chosen", () =>
|
|
27
|
+
expect(HostShellDist.resolve(~assetsDir=None))->toEqual(HostShellDist.dir())
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
testSync("is the chosen directory, created when missing", () => {
|
|
31
|
+
let chosen = NodePath.join([tmpdir("reventless-assetsdir-"), "app", "dist"])
|
|
32
|
+
expect(HostShellDist.resolve(~assetsDir=Some(chosen)))->toEqual(Some(chosen))
|
|
33
|
+
expect(NodeFs.existsSync(chosen))->toBe(true)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe("the local writers in a chosen assetsDir", () => {
|
|
38
|
+
testSync("all four write there and leave the package's dist/ alone", () => {
|
|
39
|
+
let before = snapshot()
|
|
40
|
+
let dir =
|
|
41
|
+
HostShellDist.resolve(
|
|
42
|
+
~assetsDir=Some(NodePath.join([tmpdir("reventless-assetsdir-"), "ui"])),
|
|
43
|
+
)->Option.getOrThrow
|
|
44
|
+
// The app's own config.json is the baseline the overlay goes on.
|
|
45
|
+
NodeFs.writeFileSync(NodePath.join([dir, "config.json"]), `{"appName": "App"}`)
|
|
46
|
+
let source = name => {
|
|
47
|
+
let path = NodePath.join([tmpdir("reventless-assetsdir-src-"), name])
|
|
48
|
+
NodeFs.writeFileSync(
|
|
49
|
+
path,
|
|
50
|
+
name->String.endsWith(".json") ? "{}" : "export function register() {}",
|
|
51
|
+
)
|
|
52
|
+
path
|
|
53
|
+
}
|
|
54
|
+
let bake: ReventlessInfra.Platform.bakedManifest = {components: []}
|
|
55
|
+
|
|
56
|
+
ShellConfig.emit(~bakedManifest=Some(bake), ~shellConfig=None, ~dir)
|
|
57
|
+
UiHints.emit(~uiHintsFile=Some(source("ui-hints.json")), ~dir)
|
|
58
|
+
UiSlots.emit(~uiSlotsFile=Some(source("slots.js")), ~dir)
|
|
59
|
+
BakedManifest.emit(~structures=[], ~config=bake, ~dir)
|
|
60
|
+
|
|
61
|
+
expect(NodeFs.readdirSyncNames(dir, {})->Array.toSorted(String.compare))->toEqual([
|
|
62
|
+
"component-manifest.json",
|
|
63
|
+
"config.base.json",
|
|
64
|
+
"config.json",
|
|
65
|
+
"ui-hints.json",
|
|
66
|
+
"ui-slots.js",
|
|
67
|
+
])
|
|
68
|
+
expect(snapshot())->toEqual(before)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodeos from "node:os";
|
|
5
|
+
import * as Nodepath from "node:path";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
8
|
+
import * as UiHints$ReventlessLocal from "../src/UiHints.res.mjs";
|
|
9
|
+
import * as UiSlots$ReventlessLocal from "../src/UiSlots.res.mjs";
|
|
10
|
+
import * as TestRunner$ReventlessLocal from "../src/test/TestRunner.res.mjs";
|
|
11
|
+
import * as ShellConfig$ReventlessLocal from "../src/ShellConfig.res.mjs";
|
|
12
|
+
import * as BakedManifest$ReventlessLocal from "../src/BakedManifest.res.mjs";
|
|
13
|
+
import * as HostShellDist$ReventlessLocal from "../src/HostShellDist.res.mjs";
|
|
14
|
+
|
|
15
|
+
TestRunner$ReventlessLocal.setup();
|
|
16
|
+
|
|
17
|
+
function tmpdir(prefix) {
|
|
18
|
+
return Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), prefix));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function snapshot() {
|
|
22
|
+
return Stdlib_Option.mapOr(Stdlib_Option.filter(HostShellDist$ReventlessLocal.dir(), prim => Nodefs.existsSync(prim)), [], dir => Nodefs.readdirSync(dir, {}).toSorted(Primitive_string.compare).map(name => {
|
|
23
|
+
let path = Nodepath.join(dir, name);
|
|
24
|
+
return [
|
|
25
|
+
name,
|
|
26
|
+
Nodefs.statSync(path).mtimeMs
|
|
27
|
+
];
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
globalThis.describe("HostShellDist.resolve", () => {
|
|
32
|
+
globalThis.test("is the package's dist/ when no directory is chosen", () => {
|
|
33
|
+
globalThis.expect(HostShellDist$ReventlessLocal.resolve(undefined)).toEqual(HostShellDist$ReventlessLocal.dir());
|
|
34
|
+
});
|
|
35
|
+
globalThis.test("is the chosen directory, created when missing", () => {
|
|
36
|
+
let chosen = Nodepath.join(tmpdir("reventless-assetsdir-"), "app", "dist");
|
|
37
|
+
globalThis.expect(HostShellDist$ReventlessLocal.resolve(chosen)).toEqual(chosen);
|
|
38
|
+
globalThis.expect(Nodefs.existsSync(chosen)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
globalThis.describe("the local writers in a chosen assetsDir", () => {
|
|
43
|
+
globalThis.test("all four write there and leave the package's dist/ alone", () => {
|
|
44
|
+
let before = snapshot();
|
|
45
|
+
let dir = Stdlib_Option.getOrThrow(HostShellDist$ReventlessLocal.resolve(Nodepath.join(tmpdir("reventless-assetsdir-"), "ui")), undefined);
|
|
46
|
+
Nodefs.writeFileSync(Nodepath.join(dir, "config.json"), `{"appName": "App"}`, "utf8");
|
|
47
|
+
let source = name => {
|
|
48
|
+
let path = Nodepath.join(tmpdir("reventless-assetsdir-src-"), name);
|
|
49
|
+
Nodefs.writeFileSync(path, name.endsWith(".json") ? "{}" : "export function register() {}", "utf8");
|
|
50
|
+
return path;
|
|
51
|
+
};
|
|
52
|
+
let bake_components = [];
|
|
53
|
+
let bake = {
|
|
54
|
+
components: bake_components
|
|
55
|
+
};
|
|
56
|
+
ShellConfig$ReventlessLocal.emit(bake, undefined, undefined, dir);
|
|
57
|
+
UiHints$ReventlessLocal.emit(source("ui-hints.json"), dir);
|
|
58
|
+
UiSlots$ReventlessLocal.emit(source("slots.js"), dir);
|
|
59
|
+
BakedManifest$ReventlessLocal.emit([], bake, dir);
|
|
60
|
+
globalThis.expect(Nodefs.readdirSync(dir, {}).toSorted(Primitive_string.compare)).toEqual([
|
|
61
|
+
"component-manifest.json",
|
|
62
|
+
"config.base.json",
|
|
63
|
+
"config.json",
|
|
64
|
+
"ui-hints.json",
|
|
65
|
+
"ui-slots.js"
|
|
66
|
+
]);
|
|
67
|
+
globalThis.expect(snapshot()).toEqual(before);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
tmpdir,
|
|
73
|
+
snapshot,
|
|
74
|
+
}
|
|
75
|
+
/* Not a pure module */
|
|
@@ -26,11 +26,35 @@ let threw = (f: unit => unit): bool =>
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
describe("ShellConfig.overlay", () => {
|
|
29
|
-
testSync("
|
|
29
|
+
testSync("carries only the environment when the deployment declares nothing", () => {
|
|
30
30
|
let out = ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)
|
|
31
|
-
expect(out->Dict.keysToArray)->toEqual([])
|
|
31
|
+
expect(out->Dict.keysToArray)->toEqual(["environment", "environmentPurpose"])
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
+
// The shell badges everything that is not production; locally it says so.
|
|
35
|
+
testSync("names the environment and its purpose local", () => {
|
|
36
|
+
let out = ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)
|
|
37
|
+
expect((out->str("environment"), out->str("environmentPurpose")))->toEqual((
|
|
38
|
+
Some("local"),
|
|
39
|
+
Some("local"),
|
|
40
|
+
))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
testSync("refuses a shellConfig naming either environment key", () =>
|
|
44
|
+
expect(
|
|
45
|
+
["environment", "environmentPurpose"]->Array.map(
|
|
46
|
+
key =>
|
|
47
|
+
threw(
|
|
48
|
+
() =>
|
|
49
|
+
ShellConfig.overlay(
|
|
50
|
+
~bakedManifest=None,
|
|
51
|
+
~shellConfig=Some(Dict.fromArray([(key, JSON.Encode.string("prod"))])),
|
|
52
|
+
)->ignore,
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
)->toEqual([true, true])
|
|
56
|
+
)
|
|
57
|
+
|
|
34
58
|
testSync("points manifestUrl at the bake's default key", () => {
|
|
35
59
|
let out = ShellConfig.overlay(~bakedManifest=Some(manifest()), ~shellConfig=None)
|
|
36
60
|
expect(out->str("manifestUrl"))->toEqual(Some("/component-manifest.json"))
|
|
@@ -147,11 +171,25 @@ describe("ShellConfig.emit", () => {
|
|
|
147
171
|
expect(out->str("apiEndpoint"))->toEqual(Some("/graphql"))
|
|
148
172
|
})
|
|
149
173
|
|
|
150
|
-
testSync("
|
|
174
|
+
testSync("adds only the environment for a platform that declares nothing", () => {
|
|
151
175
|
let dir = distWithConfig()
|
|
152
176
|
ShellConfig.emit(~bakedManifest=None, ~shellConfig=None, ~dir)
|
|
153
|
-
|
|
154
|
-
expect(
|
|
177
|
+
let out = readConfig(dir)
|
|
178
|
+
expect(out->Dict.keysToArray)->toEqual([
|
|
179
|
+
"apiEndpoint",
|
|
180
|
+
"appName",
|
|
181
|
+
"environment",
|
|
182
|
+
"environmentPurpose",
|
|
183
|
+
])
|
|
184
|
+
expect(readBaseline(dir)->str("appName"))->toEqual(Some("Shipped"))
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// The environment alone is no declaration: a directory with no config.json to
|
|
188
|
+
// overlay is left as it is rather than refused.
|
|
189
|
+
testSync("writes nothing and refuses nothing when only the environment has no baseline", () => {
|
|
190
|
+
let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-shellconfig-"]))
|
|
191
|
+
ShellConfig.emit(~bakedManifest=None, ~shellConfig=None, ~dir)
|
|
192
|
+
expect(NodeFs.readdirSyncNames(dir, {}))->toEqual([])
|
|
155
193
|
})
|
|
156
194
|
|
|
157
195
|
// The whole reason a baseline is kept: boot 2 overlaying boot 1's output would
|
|
@@ -273,8 +311,8 @@ describe("ShellConfig.overlay — adminGroup", () => {
|
|
|
273
311
|
testSync("is absent while the administrator role keeps its own name", () => {
|
|
274
312
|
Reventless.Role.clearGroups()
|
|
275
313
|
expect(
|
|
276
|
-
ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)->Dict.
|
|
277
|
-
)->toEqual(
|
|
314
|
+
ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)->Dict.get("adminGroup"),
|
|
315
|
+
)->toEqual(None)
|
|
278
316
|
})
|
|
279
317
|
|
|
280
318
|
testSync("carries the group a renamed administrator role maps to", () => {
|