@reventlessdev/reventless-local 3.0.0-alpha.298 → 3.0.0-alpha.299

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 (55) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +10 -10
  3. package/src/EmitCapabilities.res +3 -3
  4. package/src/LocalPlatformRegistry.res +1 -1
  5. package/src/LocalSeedTarget.res +1 -1
  6. package/src/Platform.res +57 -3
  7. package/src/Platform.res.mjs +61 -6
  8. package/src/ShellConfig.res +11 -1
  9. package/src/ShellConfig.res.mjs +10 -4
  10. package/src/adapter/Auth/Auth_GraphqlContext.res +18 -2
  11. package/src/adapter/Auth/Auth_GraphqlContext.res.mjs +8 -1
  12. package/src/adapter/Auth/UserStore.res +102 -3
  13. package/src/adapter/Auth/UserStore.res.mjs +128 -2
  14. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +33 -0
  15. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +26 -0
  16. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +11 -3
  17. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +3 -0
  18. package/src/adapter/DomainGraphQL_Server.res +1 -1
  19. package/src/adapter/GraphQL_CallerError.res +4 -0
  20. package/src/adapter/GraphQL_CallerError.res.mjs +9 -0
  21. package/src/adapter/LocalAnonymousLimit.res +35 -0
  22. package/src/adapter/LocalAnonymousLimit.res.mjs +52 -0
  23. package/src/adapter/LocalCapabilities.res +14 -2
  24. package/src/adapter/LocalCapabilities.res.mjs +21 -5
  25. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +22 -0
  26. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +7 -0
  27. package/src/components/InboundTranslationSlice_Builder.res +1 -0
  28. package/src/components/InboundTranslationSlice_Builder.res.mjs +4 -1
  29. package/tests/EmitCapabilitiesTest.res +4 -6
  30. package/tests/EmitCapabilitiesTest.res.mjs +1 -2
  31. package/tests/HostShellDistTest.res.mjs +1 -1
  32. package/tests/LocalPlatformRegistryTest.res +3 -5
  33. package/tests/LocalPlatformRegistryTest.res.mjs +2 -4
  34. package/tests/LocalPlatformStartTest.res +2 -2
  35. package/tests/LocalPlatformStartTest.res.mjs +1 -2
  36. package/tests/ShellConfigTest.res.mjs +27 -27
  37. package/tests/adapter/InboundTranslationMutationTest.res +13 -2
  38. package/tests/adapter/InboundTranslationMutationTest.res.mjs +7 -4
  39. package/tests/adapter/LocalAnonymousLimitTest.res +70 -0
  40. package/tests/adapter/LocalAnonymousLimitTest.res.mjs +85 -0
  41. package/tests/adapter/LocalAuthLoginTest.res +2 -2
  42. package/tests/adapter/LocalAuthUserStoreTest.res +88 -0
  43. package/tests/adapter/LocalAuthUserStoreTest.res.mjs +83 -0
  44. package/tests/capability/LocalCapabilitiesTest.res +2 -1
  45. package/tests/capability/LocalCapabilitiesTest.res.mjs +3 -2
  46. package/tests/components/inboundtranslationslice/InboundTranslationCredentialTest.res +136 -0
  47. package/tests/components/inboundtranslationslice/InboundTranslationCredentialTest.res.mjs +183 -0
  48. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +143 -11
  49. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +129 -22
  50. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +18 -5
  51. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +12 -3
  52. package/tests/components/inboundtranslationslice/InboundTranslationWebhookTest.res +26 -6
  53. package/tests/components/inboundtranslationslice/InboundTranslationWebhookTest.res.mjs +48 -13
  54. package/tests/plugin/Plugin_Fixtures.res +0 -4
  55. package/tests/plugin/Plugin_Fixtures.res.mjs +13 -33
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
4
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
5
+ import * as LocalCapabilities$ReventlessLocal from "../adapter/LocalCapabilities.res.mjs";
5
6
  import * as LocalQueryDbStorage$ReventlessLocal from "../adapter/QueryDb/LocalQueryDbStorage.res.mjs";
6
7
  import * as LocalWebhookRegistry$ReventlessLocal from "../adapter/Webhook/LocalWebhookRegistry.res.mjs";
7
8
  import * as QueryDbResolvers_GraphQL$ReventlessLocal from "../adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs";
@@ -21,7 +22,9 @@ function Make(Bus) {
21
22
  make: QueryDbStorage.make
22
23
  })({
23
24
  make: QueryDbResolvers.make
24
- })(Api);
25
+ })(Api)({
26
+ capabilities: LocalCapabilities$ReventlessLocal.capabilities
27
+ });
25
28
  let Make$1 = Spec => (Translation => {
26
29
  let Inner = CoreMaker.Make(Spec)(Translation);
27
30
  let make = (publishJsons, runtime, opts) => {
@@ -5,14 +5,12 @@ open JestGlobals
5
5
 
6
6
  describe("EmitCapabilities.parseArgs", () => {
7
7
  testSync("a plugin's postbuild: emit-capabilities src/", () =>
8
- expect(EmitCapabilities.parseArgs(["src/"]))->toEqual(
9
- Ok({EmitCapabilities.srcDir: "src/", compositionModule: None}),
10
- )
8
+ expect(EmitCapabilities.parseArgs(["src/"]))->toEqual(Ok({EmitCapabilities.srcDir: "src/"}))
11
9
  )
12
10
 
13
11
  testSync("a hand-written composition root is named after the source directory", () =>
14
12
  expect(EmitCapabilities.parseArgs(["src", "Catalog"]))->toEqual(
15
- Ok({EmitCapabilities.srcDir: "src", compositionModule: Some("Catalog")}),
13
+ Ok({EmitCapabilities.srcDir: "src", compositionModule: "Catalog"}),
16
14
  )
17
15
  )
18
16
 
@@ -29,7 +27,7 @@ describe("emit-capabilities on the command line", () => {
29
27
  expect(await CliArgs.observe(EmitCapabilities.cli, ["src/", "--help"]))->toEqual({
30
28
  CliArgs.stdout: [usage],
31
29
  stderr: [],
32
- exitCode: Some(0),
30
+ exitCode: 0,
33
31
  reachedMain: false,
34
32
  })
35
33
  )
@@ -38,7 +36,7 @@ describe("emit-capabilities on the command line", () => {
38
36
  expect(await CliArgs.observe(EmitCapabilities.cli, ["src", "Catalog", "extra"]))->toEqual({
39
37
  CliArgs.stdout: [],
40
38
  stderr: [`emit-capabilities: unknown argument "extra"\n\n${usage}`],
41
- exitCode: Some(2),
39
+ exitCode: 2,
42
40
  reachedMain: false,
43
41
  })
44
42
  )
@@ -9,8 +9,7 @@ globalThis.describe("EmitCapabilities.parseArgs", () => {
9
9
  globalThis.expect(EmitCapabilities$ReventlessLocal.parseArgs(["src/"])).toEqual({
10
10
  TAG: "Ok",
11
11
  _0: {
12
- srcDir: "src/",
13
- compositionModule: undefined
12
+ srcDir: "src/"
14
13
  }
15
14
  });
16
15
  });
@@ -53,7 +53,7 @@ globalThis.describe("the local writers in a chosen assetsDir", () => {
53
53
  let bake = {
54
54
  components: bake_components
55
55
  };
56
- ShellConfig$ReventlessLocal.emit(bake, undefined, undefined, dir);
56
+ ShellConfig$ReventlessLocal.emit(bake, undefined, undefined, undefined, dir);
57
57
  UiHints$ReventlessLocal.emit(source("ui-hints.json"), dir);
58
58
  UiSlots$ReventlessLocal.emit(source("slots.js"), dir);
59
59
  BakedManifest$ReventlessLocal.emit([], bake, dir);
@@ -17,8 +17,8 @@ let tempRoot = (): string =>
17
17
  // rather than hitting an unrelated process that happens to be running.
18
18
  let deadPid = 2147483646
19
19
 
20
- let sqliteStore = (path): LocalPlatformRegistry.store => {kind: "sqlite", path: Some(path)}
21
- let memoryStore: LocalPlatformRegistry.store = {kind: "memory", path: None}
20
+ let sqliteStore = (path): LocalPlatformRegistry.store => {kind: "sqlite", path}
21
+ let memoryStore: LocalPlatformRegistry.store = {kind: "memory"}
22
22
 
23
23
  let writeAt = (~cwd, ~port, ~store, ~pid=NodeProcess.pid) =>
24
24
  LocalPlatformRegistry.write(
@@ -127,9 +127,7 @@ describe("LocalSeedTarget.storePath", () => {
127
127
 
128
128
  expect(target(memoryStore)->reason->String.includes("in memory"))->toEqual(true)
129
129
  expect(target(memoryStore)->reason->String.includes("localhost:4000"))->toEqual(true)
130
- expect(
131
- target({kind: "postgres", path: None})->reason->String.includes("off this machine"),
132
- )->toEqual(true)
130
+ expect(target({kind: "postgres"})->reason->String.includes("off this machine"))->toEqual(true)
133
131
  expect(
134
132
  {
135
133
  endpoint: "http://localhost:4000/graphql",
@@ -19,8 +19,7 @@ function sqliteStore(path) {
19
19
  }
20
20
 
21
21
  let memoryStore = {
22
- kind: "memory",
23
- path: undefined
22
+ kind: "memory"
24
23
  };
25
24
 
26
25
  function writeAt(cwd, port, store, pidOpt) {
@@ -129,8 +128,7 @@ globalThis.describe("LocalSeedTarget.storePath", () => {
129
128
  globalThis.expect(reason(target(memoryStore)).includes("in memory")).toEqual(true);
130
129
  globalThis.expect(reason(target(memoryStore)).includes("localhost:4000")).toEqual(true);
131
130
  globalThis.expect(reason(target({
132
- kind: "postgres",
133
- path: undefined
131
+ kind: "postgres"
134
132
  })).includes("off this machine")).toEqual(true);
135
133
  globalThis.expect(reason({
136
134
  endpoint: "http://localhost:4000/graphql",
@@ -10,8 +10,8 @@ let tempRoot = (): string =>
10
10
 
11
11
  let deadPid = 2147483646
12
12
 
13
- let sqliteStore = (path): LocalPlatformRegistry.store => {kind: "sqlite", path: Some(path)}
14
- let memoryStore: LocalPlatformRegistry.store = {kind: "memory", path: None}
13
+ let sqliteStore = (path): LocalPlatformRegistry.store => {kind: "sqlite", path}
14
+ let memoryStore: LocalPlatformRegistry.store = {kind: "memory"}
15
15
 
16
16
  let writeAt = (~cwd, ~port, ~store, ~pid=NodeProcess.pid) =>
17
17
  LocalPlatformRegistry.write(
@@ -23,8 +23,7 @@ function sqliteStore(path) {
23
23
  }
24
24
 
25
25
  let memoryStore = {
26
- kind: "memory",
27
- path: undefined
26
+ kind: "memory"
28
27
  };
29
28
 
30
29
  function writeAt(cwd, port, store, pidOpt) {
@@ -37,14 +37,14 @@ function threw(f) {
37
37
 
38
38
  globalThis.describe("ShellConfig.overlay", () => {
39
39
  globalThis.test("carries only the environment when the deployment declares nothing", () => {
40
- let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
40
+ let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined, undefined);
41
41
  globalThis.expect(Object.keys(out)).toEqual([
42
42
  "environment",
43
43
  "environmentPurpose"
44
44
  ]);
45
45
  });
46
46
  globalThis.test("names the environment and its purpose local", () => {
47
- let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
47
+ let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined, undefined);
48
48
  globalThis.expect([
49
49
  str(out, "environment"),
50
50
  str(out, "environmentPurpose")
@@ -61,18 +61,18 @@ globalThis.describe("ShellConfig.overlay", () => {
61
61
  ShellConfig$ReventlessLocal.overlay(undefined, undefined, Object.fromEntries([[
62
62
  key,
63
63
  "prod"
64
- ]]));
64
+ ]]), undefined);
65
65
  }))).toEqual([
66
66
  true,
67
67
  true
68
68
  ]);
69
69
  });
70
70
  globalThis.test("points manifestUrl at the bake's default key", () => {
71
- let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined, undefined);
71
+ let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined, undefined, undefined);
72
72
  globalThis.expect(str(out, "manifestUrl")).toEqual("/component-manifest.json");
73
73
  });
74
74
  globalThis.test("follows a renamed bake", () => {
75
- let out = ShellConfig$ReventlessLocal.overlay(manifest("storefront.json"), undefined, undefined);
75
+ let out = ShellConfig$ReventlessLocal.overlay(manifest("storefront.json"), undefined, undefined, undefined);
76
76
  globalThis.expect(str(out, "manifestUrl")).toEqual("/storefront.json");
77
77
  });
78
78
  globalThis.test("passes the deployment's own keys through verbatim", () => {
@@ -85,7 +85,7 @@ globalThis.describe("ShellConfig.overlay", () => {
85
85
  "elevatedGroups",
86
86
  ["Admin"].map(prim => prim)
87
87
  ]
88
- ]));
88
+ ]), undefined);
89
89
  globalThis.expect(str(out, "appName")).toEqual("Online Shop");
90
90
  globalThis.expect(Stdlib_Option.isSome(out["elevatedGroups"])).toBe(true);
91
91
  });
@@ -94,22 +94,22 @@ globalThis.describe("ShellConfig.overlay", () => {
94
94
  ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined, Object.fromEntries([[
95
95
  "manifestUrl",
96
96
  "/elsewhere.json"
97
- ]]));
97
+ ]]), undefined);
98
98
  })).toBe(true);
99
99
  });
100
100
  });
101
101
 
102
102
  globalThis.describe("ShellConfig.overlay — uiSlotsFile", () => {
103
103
  globalThis.test("unset ⇒ no uiSlotsUrl", () => {
104
- let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
104
+ let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined, undefined);
105
105
  globalThis.expect(Stdlib_Option.isNone(out["uiSlotsUrl"])).toBe(true);
106
106
  });
107
107
  globalThis.test("declared ⇒ the URL the platform serves the module from", () => {
108
- let out = ShellConfig$ReventlessLocal.overlay(undefined, "/src/storefront-slots.js", undefined);
108
+ let out = ShellConfig$ReventlessLocal.overlay(undefined, "/src/storefront-slots.js", undefined, undefined);
109
109
  globalThis.expect(out["uiSlotsUrl"]).toEqual("/ui-slots.js");
110
110
  });
111
111
  globalThis.test("withdrawn ⇒ the overlay stops naming it, so the baseline restores", () => {
112
- let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
112
+ let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined, undefined);
113
113
  globalThis.expect(Object.keys(out).includes("uiSlotsUrl")).toBe(false);
114
114
  });
115
115
  globalThis.test("refuses a shellConfig key redirecting the computed module", () => {
@@ -117,7 +117,7 @@ globalThis.describe("ShellConfig.overlay — uiSlotsFile", () => {
117
117
  ShellConfig$ReventlessLocal.overlay(undefined, "/src/storefront-slots.js", Object.fromEntries([[
118
118
  "uiSlotsUrl",
119
119
  "/elsewhere.js"
120
- ]]));
120
+ ]]), undefined);
121
121
  })).toBe(true);
122
122
  });
123
123
  });
@@ -137,14 +137,14 @@ globalThis.describe("ShellConfig.emit", () => {
137
137
  ]]);
138
138
  globalThis.test("merges the overlay onto the shipped file and keeps its other keys", () => {
139
139
  let dir = distWithConfig();
140
- ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, undefined, dir);
140
+ ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, undefined, undefined, dir);
141
141
  let out = readConfig(dir);
142
142
  globalThis.expect(str(out, "manifestUrl")).toEqual("/component-manifest.json");
143
143
  globalThis.expect(str(out, "apiEndpoint")).toEqual("/graphql");
144
144
  });
145
145
  globalThis.test("adds only the environment for a platform that declares nothing", () => {
146
146
  let dir = distWithConfig();
147
- ShellConfig$ReventlessLocal.emit(undefined, undefined, undefined, dir);
147
+ ShellConfig$ReventlessLocal.emit(undefined, undefined, undefined, undefined, dir);
148
148
  let out = readConfig(dir);
149
149
  globalThis.expect(Object.keys(out)).toEqual([
150
150
  "apiEndpoint",
@@ -156,27 +156,27 @@ globalThis.describe("ShellConfig.emit", () => {
156
156
  });
157
157
  globalThis.test("writes nothing and refuses nothing when only the environment has no baseline", () => {
158
158
  let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-shellconfig-"));
159
- ShellConfig$ReventlessLocal.emit(undefined, undefined, undefined, dir);
159
+ ShellConfig$ReventlessLocal.emit(undefined, undefined, undefined, undefined, dir);
160
160
  globalThis.expect(Nodefs.readdirSync(dir, {})).toEqual([]);
161
161
  });
162
162
  globalThis.test("starts every boot from the shipped file, not the last output", () => {
163
163
  let dir = distWithConfig();
164
- ShellConfig$ReventlessLocal.emit(undefined, undefined, named("First"), dir);
165
- ShellConfig$ReventlessLocal.emit(undefined, undefined, named("Second"), dir);
164
+ ShellConfig$ReventlessLocal.emit(undefined, undefined, named("First"), undefined, dir);
165
+ ShellConfig$ReventlessLocal.emit(undefined, undefined, named("Second"), undefined, dir);
166
166
  globalThis.expect(str(readConfig(dir), "appName")).toEqual("Second");
167
167
  globalThis.expect(str(readBaseline(dir), "appName")).toEqual("Shipped");
168
168
  });
169
169
  globalThis.test("restores the shipped file once the declaration is withdrawn", () => {
170
170
  let dir = distWithConfig();
171
- ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, undefined, dir);
172
- ShellConfig$ReventlessLocal.emit(undefined, undefined, undefined, dir);
171
+ ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, undefined, undefined, dir);
172
+ ShellConfig$ReventlessLocal.emit(undefined, undefined, undefined, undefined, dir);
173
173
  let out = readConfig(dir);
174
174
  globalThis.expect(out["manifestUrl"]).toEqual(undefined);
175
175
  globalThis.expect(str(out, "appName")).toEqual("Shipped");
176
176
  });
177
177
  globalThis.test("refuses to overlay a dist that ships no config.json", () => {
178
178
  let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-shellconfig-"));
179
- globalThis.expect(threw(() => ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, undefined, dir))).toBe(true);
179
+ globalThis.expect(threw(() => ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, undefined, undefined, dir))).toBe(true);
180
180
  });
181
181
  });
182
182
 
@@ -218,22 +218,22 @@ let fulfilment = {
218
218
 
219
219
  globalThis.describe("ShellConfig.overlay — journeys", () => {
220
220
  globalThis.test("a bake declaring no journeys writes no map", () => {
221
- let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined, undefined);
221
+ let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined, undefined, undefined);
222
222
  globalThis.expect(out["journeyManifestUrls"]).toEqual(undefined);
223
223
  });
224
224
  globalThis.test("an empty journeys array is the same as none", () => {
225
- let out = ShellConfig$ReventlessLocal.overlay(withJourneys([]), undefined, undefined);
225
+ let out = ShellConfig$ReventlessLocal.overlay(withJourneys([]), undefined, undefined, undefined);
226
226
  globalThis.expect(out["journeyManifestUrls"]).toEqual(undefined);
227
227
  });
228
228
  globalThis.test("keeps manifestUrl as the default journey", () => {
229
- let out = ShellConfig$ReventlessLocal.overlay(withJourneys([shopper]), undefined, undefined);
229
+ let out = ShellConfig$ReventlessLocal.overlay(withJourneys([shopper]), undefined, undefined, undefined);
230
230
  globalThis.expect(str(out, "manifestUrl")).toEqual("/component-manifest.json");
231
231
  });
232
232
  globalThis.test("maps each declared group to its own file", () => {
233
233
  let out = ShellConfig$ReventlessLocal.overlay(withJourneys([
234
234
  shopper,
235
235
  fulfilment
236
- ]), undefined, undefined);
236
+ ]), undefined, undefined, undefined);
237
237
  let map = Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(out["journeyManifestUrls"], Stdlib_JSON.Decode.object), undefined);
238
238
  globalThis.expect([
239
239
  str(map, "Shopper"),
@@ -248,7 +248,7 @@ globalThis.describe("ShellConfig.overlay — journeys", () => {
248
248
  ShellConfig$ReventlessLocal.overlay(withJourneys([shopper]), undefined, Object.fromEntries([[
249
249
  "journeyManifestUrls",
250
250
  {}
251
- ]]));
251
+ ]]), undefined);
252
252
  })).toBe(true);
253
253
  });
254
254
  });
@@ -256,7 +256,7 @@ globalThis.describe("ShellConfig.overlay — journeys", () => {
256
256
  globalThis.describe("ShellConfig.overlay — adminGroup", () => {
257
257
  globalThis.test("is absent while the administrator role keeps its own name", () => {
258
258
  Role$Reventless.clearGroups();
259
- globalThis.expect(ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined)["adminGroup"]).toEqual(undefined);
259
+ globalThis.expect(ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined, undefined)["adminGroup"]).toEqual(undefined);
260
260
  });
261
261
  globalThis.test("carries the group a renamed administrator role maps to", () => {
262
262
  Role$Reventless.clearGroups();
@@ -264,7 +264,7 @@ globalThis.describe("ShellConfig.overlay — adminGroup", () => {
264
264
  Role$Reventless.admin,
265
265
  "shop-admins"
266
266
  ]]);
267
- let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
267
+ let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined, undefined);
268
268
  Role$Reventless.clearGroups();
269
269
  globalThis.expect(str(out, "adminGroup")).toEqual("shop-admins");
270
270
  });
@@ -273,7 +273,7 @@ globalThis.describe("ShellConfig.overlay — adminGroup", () => {
273
273
  ShellConfig$ReventlessLocal.overlay(undefined, undefined, Object.fromEntries([[
274
274
  "adminGroup",
275
275
  "x"
276
- ]]));
276
+ ]]), undefined);
277
277
  })).toBe(true);
278
278
  });
279
279
  });
@@ -118,7 +118,13 @@ describe("InboundTranslationSlice mutation — response envelope", () => {
118
118
 
119
119
  InboundTranslationResolvers_GraphQL.bindReceive(
120
120
  ~fieldName,
121
- ~receive=(inputJson, ~caller=?) => Callback.receive(publishJsons, inputJson, ~caller?),
121
+ ~receive=(inputJson, ~caller=?) =>
122
+ Callback.receive(
123
+ publishJsons,
124
+ inputJson,
125
+ ~capabilities=Reventless.Capabilities.none,
126
+ ~caller?,
127
+ ),
122
128
  )
123
129
  })
124
130
 
@@ -221,7 +227,12 @@ describe("InboundTranslationSlice mutation — the door", () => {
221
227
  InboundTranslationResolvers_GraphQL.bindReceive(
222
228
  ~fieldName=feedField,
223
229
  ~receive=(inputJson, ~caller=?) =>
224
- CatalogFeedCallback.receive(publishJsons, inputJson, ~caller?),
230
+ CatalogFeedCallback.receive(
231
+ publishJsons,
232
+ inputJson,
233
+ ~capabilities=Reventless.Capabilities.none,
234
+ ~caller?,
235
+ ),
225
236
  )
226
237
  })
227
238
 
@@ -8,6 +8,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
8
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
9
9
  import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
10
10
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
11
+ import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
11
12
  import * as Dcb_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/Dcb/Dcb_Builder.res.mjs";
12
13
  import * as DomainGraphQL_Server$ReventlessLocal from "../../src/adapter/DomainGraphQL_Server.res.mjs";
13
14
  import * as InboundTranslationSliceFixtures$ReventlessLocal from "../components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs";
@@ -31,7 +32,8 @@ let Callback = InboundTranslationSlice_Callback$ReventlessCore.Make({
31
32
  targetName: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.targetName,
32
33
  externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.externalSystem,
33
34
  authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.authorizationOf,
34
- endpoint: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.endpoint
35
+ endpoint: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.endpoint,
36
+ capabilityNeeds: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.capabilityNeeds
35
37
  })(PaymentWebhookTranslation);
36
38
 
37
39
  let scope = "Payments";
@@ -113,7 +115,7 @@ globalThis.describe("InboundTranslationSlice mutation — response envelope", ()
113
115
  DomainGraphQL_Server$ReventlessLocal.setScope(scope);
114
116
  InboundTranslationResolvers_GraphQL$ReventlessLocal.register(fieldName, InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.externalInputSchema, "AllowAuthenticated", DomainGraphQL_Server$ReventlessLocal.asInterface);
115
117
  DomainGraphQL_Server$ReventlessLocal.resetScope();
116
- InboundTranslationResolvers_GraphQL$ReventlessLocal.bindReceive(fieldName, (inputJson, caller) => Callback.receive(publishJsons, inputJson, caller, undefined));
118
+ InboundTranslationResolvers_GraphQL$ReventlessLocal.bindReceive(fieldName, (inputJson, caller) => Callback.receive(publishJsons, inputJson, Capabilities$Reventless.none, caller, undefined));
117
119
  });
118
120
  globalThis.test("a successful translation resolves as CommandAccepted, with no errors", async () => {
119
121
  let result = await runMutation("completed", undefined);
@@ -181,7 +183,8 @@ let CatalogFeedCallback = InboundTranslationSlice_Callback$ReventlessCore.Make({
181
183
  targetName: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.targetName,
182
184
  externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.externalSystem,
183
185
  authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.authorizationOf,
184
- endpoint: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.endpoint
186
+ endpoint: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.endpoint,
187
+ capabilityNeeds: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.capabilityNeeds
185
188
  })(CatalogFeedTranslation);
186
189
 
187
190
  let feedField = "Catalog_CatalogFeed";
@@ -197,7 +200,7 @@ globalThis.describe("InboundTranslationSlice mutation — the door", () => {
197
200
  DomainGraphQL_Server$ReventlessLocal.setScope("Catalog");
198
201
  InboundTranslationResolvers_GraphQL$ReventlessLocal.register(feedField, InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.externalInputSchema, Dcb_Builder$ReventlessCore.inboundDoorPermission(InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.commandSchema, InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.authorizationOf), DomainGraphQL_Server$ReventlessLocal.asInterface);
199
202
  DomainGraphQL_Server$ReventlessLocal.resetScope();
200
- InboundTranslationResolvers_GraphQL$ReventlessLocal.bindReceive(feedField, (inputJson, caller) => CatalogFeedCallback.receive(publishJsons, inputJson, caller, undefined));
203
+ InboundTranslationResolvers_GraphQL$ReventlessLocal.bindReceive(feedField, (inputJson, caller) => CatalogFeedCallback.receive(publishJsons, inputJson, Capabilities$Reventless.none, caller, undefined));
201
204
  });
202
205
  globalThis.test("a caller holding none of the commands' roles is refused", async () => {
203
206
  let node = payload(await runFeed(signedIn(["Ops"]), "product"));
@@ -0,0 +1,70 @@
1
+ open JestGlobals
2
+
3
+ // The local doors' limit, as the resolvers apply it: only a rule that admits
4
+ // anonymous callers is counted, and the answer is a `Throttled` rejection.
5
+
6
+ @set_index external setEnv: (dict<string>, string, string) => unit = ""
7
+
8
+ let ctxFrom = (address: string): JSON.t =>
9
+ JSON.Encode.object(Dict.fromArray([("sourceAddress", JSON.Encode.string(address))]))
10
+
11
+ let throttled = (~rule, ~ctx) =>
12
+ CommandGeneratorResolvers_GraphQL.throttled(~rule, ~field="Registration_SignUp", ~ctx)
13
+
14
+ describe("the limit on anonymous doors", () => {
15
+ beforeEach(() => {
16
+ NodeProcess.env->setEnv(Reventless.AnonymousRequestLimit.envKey, "2/3600")
17
+ LocalAnonymousLimit.reset()
18
+ })
19
+ afterEach(() => {
20
+ NodeProcess.env->Dict.delete(Reventless.AnonymousRequestLimit.envKey)
21
+ LocalAnonymousLimit.reset()
22
+ })
23
+
24
+ testSync("an anonymous door is limited per source address", () => {
25
+ let ctx = ctxFrom("10.0.0.1")
26
+ let answers = [
27
+ throttled(~rule=AllowAnonymous, ~ctx)->Option.isSome,
28
+ throttled(~rule=AllowAnonymous, ~ctx)->Option.isSome,
29
+ throttled(~rule=AllowAnonymous, ~ctx)->Option.isSome,
30
+ ]
31
+ expect(answers)->toEqual([false, false, true])
32
+ })
33
+
34
+ testSync("another source is not held back by the first", () => {
35
+ let _ = throttled(~rule=AllowAnonymous, ~ctx=ctxFrom("10.0.0.1"))
36
+ let _ = throttled(~rule=AllowAnonymous, ~ctx=ctxFrom("10.0.0.1"))
37
+ expect(throttled(~rule=AllowAnonymous, ~ctx=ctxFrom("10.0.0.2"))->Option.isSome)->toBe(false)
38
+ })
39
+
40
+ testSync("a door for signed-in callers is never counted", () => {
41
+ let ctx = ctxFrom("10.0.0.1")
42
+ let answers = Array.fromInitializer(
43
+ ~length=5,
44
+ _ => throttled(~rule=AllowAuthenticated, ~ctx)->Option.isSome,
45
+ )
46
+ expect(answers->Array.some(x => x))->toBe(false)
47
+ })
48
+
49
+ testSync("the refusal is a Throttled rejection, retryable by its cause", () => {
50
+ let ctx = ctxFrom("10.0.0.1")
51
+ let _ = throttled(~rule=AllowAnonymous, ~ctx)
52
+ let _ = throttled(~rule=AllowAnonymous, ~ctx)
53
+ let rejection = throttled(~rule=AllowAnonymous, ~ctx)->Option.getOr(JSON.Encode.null)
54
+ let text = rejection->JSON.stringify
55
+ expect((text->String.includes("Throttled"), text->String.includes("INFRASTRUCTURE")))->toEqual((
56
+ true,
57
+ true,
58
+ ))
59
+ })
60
+
61
+ testSync("a mistyped limit fails rather than becoming the default", () => {
62
+ NodeProcess.env->setEnv(Reventless.AnonymousRequestLimit.envKey, "ten")
63
+ LocalAnonymousLimit.reset()
64
+ let threw = switch throttled(~rule=AllowAnonymous, ~ctx=ctxFrom("10.0.0.1")) {
65
+ | _ => false
66
+ | exception _ => true
67
+ }
68
+ expect(threw)->toBe(true)
69
+ })
70
+ })
@@ -0,0 +1,85 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as AnonymousRequestLimit$Reventless from "@reventlessdev/reventless-spec/src/types/AnonymousRequestLimit.res.mjs";
7
+ import * as LocalAnonymousLimit$ReventlessLocal from "../../src/adapter/LocalAnonymousLimit.res.mjs";
8
+ import * as CommandGeneratorResolvers_GraphQL$ReventlessLocal from "../../src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs";
9
+
10
+ function ctxFrom(address) {
11
+ return Object.fromEntries([[
12
+ "sourceAddress",
13
+ address
14
+ ]]);
15
+ }
16
+
17
+ function throttled(rule, ctx) {
18
+ return CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled(rule, "Registration_SignUp", ctx);
19
+ }
20
+
21
+ globalThis.describe("the limit on anonymous doors", () => {
22
+ globalThis.beforeEach(() => {
23
+ process.env[AnonymousRequestLimit$Reventless.envKey] = "2/3600";
24
+ LocalAnonymousLimit$ReventlessLocal.reset();
25
+ });
26
+ globalThis.afterEach(() => {
27
+ Stdlib_Dict.$$delete(process.env, AnonymousRequestLimit$Reventless.envKey);
28
+ LocalAnonymousLimit$ReventlessLocal.reset();
29
+ });
30
+ globalThis.test("an anonymous door is limited per source address", () => {
31
+ let ctx = ctxFrom("10.0.0.1");
32
+ let answers = [
33
+ Stdlib_Option.isSome(CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAnonymous", "Registration_SignUp", ctx)),
34
+ Stdlib_Option.isSome(CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAnonymous", "Registration_SignUp", ctx)),
35
+ Stdlib_Option.isSome(CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAnonymous", "Registration_SignUp", ctx))
36
+ ];
37
+ globalThis.expect(answers).toEqual([
38
+ false,
39
+ false,
40
+ true
41
+ ]);
42
+ });
43
+ globalThis.test("another source is not held back by the first", () => {
44
+ CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAnonymous", "Registration_SignUp", ctxFrom("10.0.0.1"));
45
+ CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAnonymous", "Registration_SignUp", ctxFrom("10.0.0.1"));
46
+ globalThis.expect(Stdlib_Option.isSome(CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAnonymous", "Registration_SignUp", ctxFrom("10.0.0.2")))).toBe(false);
47
+ });
48
+ globalThis.test("a door for signed-in callers is never counted", () => {
49
+ let ctx = ctxFrom("10.0.0.1");
50
+ let answers = Stdlib_Array.fromInitializer(5, param => Stdlib_Option.isSome(CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAuthenticated", "Registration_SignUp", ctx)));
51
+ globalThis.expect(answers.some(x => x)).toBe(false);
52
+ });
53
+ globalThis.test("the refusal is a Throttled rejection, retryable by its cause", () => {
54
+ let ctx = ctxFrom("10.0.0.1");
55
+ CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAnonymous", "Registration_SignUp", ctx);
56
+ CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAnonymous", "Registration_SignUp", ctx);
57
+ let rejection = Stdlib_Option.getOr(CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAnonymous", "Registration_SignUp", ctx), null);
58
+ let text = JSON.stringify(rejection);
59
+ globalThis.expect([
60
+ text.includes("Throttled"),
61
+ text.includes("INFRASTRUCTURE")
62
+ ]).toEqual([
63
+ true,
64
+ true
65
+ ]);
66
+ });
67
+ globalThis.test("a mistyped limit fails rather than becoming the default", () => {
68
+ process.env[AnonymousRequestLimit$Reventless.envKey] = "ten";
69
+ LocalAnonymousLimit$ReventlessLocal.reset();
70
+ let threw;
71
+ try {
72
+ CommandGeneratorResolvers_GraphQL$ReventlessLocal.throttled("AllowAnonymous", "Registration_SignUp", ctxFrom("10.0.0.1"));
73
+ threw = false;
74
+ } catch (exn) {
75
+ threw = true;
76
+ }
77
+ globalThis.expect(threw).toBe(true);
78
+ });
79
+ });
80
+
81
+ export {
82
+ ctxFrom,
83
+ throttled,
84
+ }
85
+ /* Not a pure module */
@@ -394,8 +394,8 @@ describe("the conformance table, minted locally", () => {
394
394
  ReventlessCore.Auth_ActiveRole.conformanceCases->Array.forEach(({
395
395
  label,
396
396
  membership,
397
- requested,
398
- expected,
397
+ ?requested,
398
+ ?expected,
399
399
  }) =>
400
400
  testPromise(
401
401
  label,