@reventlessdev/reventless-core 3.0.0-alpha.223 → 3.0.0-alpha.225

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 (51) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +6 -6
  3. package/src/admin/Platform_Admin_Structure.res +16 -0
  4. package/src/admin/Platform_Admin_Structure.res.mjs +14 -3
  5. package/src/admin/Platform_BakedManifest.res +154 -0
  6. package/src/admin/Platform_BakedManifest.res.mjs +156 -0
  7. package/src/admin/Platform_ComponentDefinitionsApi.res +19 -2
  8. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +30 -2
  9. package/src/components/Api/Api_Ids.res +63 -0
  10. package/src/components/Api/Api_Ids.res.mjs +43 -0
  11. package/src/components/Api/GraphQL_FragmentGenerator.res +121 -28
  12. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +97 -27
  13. package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
  14. package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
  15. package/src/components/Api/QueryDbListQuery.res +38 -6
  16. package/src/components/Api/QueryDbListQuery.res.mjs +14 -5
  17. package/src/components/Api/SchemaType.res +36 -2
  18. package/src/components/Api/SchemaType.res.mjs +31 -10
  19. package/src/components/Api/SuryToJsonSchema.res +19 -0
  20. package/src/components/Api/SuryToJsonSchema.res.mjs +18 -4
  21. package/src/components/CommandGenerator/CommandGenerator_Callback.res +48 -0
  22. package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +27 -1
  23. package/src/plugin/component/Plugin_Structure.res +78 -3
  24. package/src/plugin/component/Plugin_Structure.res.mjs +38 -9
  25. package/tests/admin/Platform_BakedManifestTest.res +244 -0
  26. package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
  27. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +73 -0
  28. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +135 -6
  29. package/tests/admin/Platform_PluginStructuresApiTest.res +12 -0
  30. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +27 -2
  31. package/tests/api/Api_IdsTest.res +143 -0
  32. package/tests/api/Api_IdsTest.res.mjs +97 -0
  33. package/tests/api/GraphQL_FragmentGeneratorTest.res +169 -0
  34. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +179 -0
  35. package/tests/api/OwnerScopeDiagnosticsTest.res +109 -0
  36. package/tests/api/OwnerScopeDiagnosticsTest.res.mjs +91 -0
  37. package/tests/api/SuryToJsonSchemaTest.res +45 -0
  38. package/tests/api/SuryToJsonSchemaTest.res.mjs +23 -0
  39. package/tests/commandgenerator/OwnerStampingTest.res +174 -0
  40. package/tests/commandgenerator/OwnerStampingTest.res.mjs +195 -0
  41. package/tests/plugin/PluginStructureAccessTest.res +110 -0
  42. package/tests/plugin/PluginStructureAccessTest.res.mjs +144 -0
  43. package/tests/plugin/PluginStructureTest.res +90 -0
  44. package/tests/plugin/PluginStructureTest.res.mjs +86 -8
  45. package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
  46. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
  47. package/tests/plugin/StateViewSlice/PsCategoriesView.res +17 -0
  48. package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +57 -0
  49. package/tests/plugin/StateViewSlice/PsGatedView.res +15 -0
  50. package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +60 -0
  51. package/tests/plugin/pluginDefinitionRequiredScalars.txt +4 -0
@@ -0,0 +1,174 @@
1
+ open JestGlobals
2
+
3
+ S.enableJson()
4
+
5
+ // Driven through `makeGenerateCommand` rather than by calling `stampOwnerFields`
6
+ // directly. The unit under test is not really the stamp — it is the claim that
7
+ // the stamp is reached on the path a command actually takes, which is the half a
8
+ // direct call cannot establish. Both transports funnel through this function, so
9
+ // what passes here is true of the local resolver and of AppSync alike.
10
+
11
+ @schema
12
+ type command =
13
+ | PlaceOrder({
14
+ orderId: @s.matches(Reventless.DcbTag.string) string,
15
+ customerId: @s.matches(Reventless.Owner.string) string,
16
+ note: string,
17
+ })
18
+ | // The control variant: same union, no owner. Whatever the caller sends here
19
+ // must survive untouched, or the stamp is reaching across constructors.
20
+ ImportProducts({batchId: @s.matches(Reventless.DcbTag.string) string, source: string})
21
+
22
+ let published: ref<array<Message.commandJson>> = ref([])
23
+
24
+ let generate = CommandGenerator_Callback.makeGenerateCommand(
25
+ ~publishJsons=async cmds => published := published.contents->Array.concat(cmds),
26
+ ~serviceName="Ordering",
27
+ ~commandSchema=commandSchema->S.castToUnknown,
28
+ ~componentKind=CommandGenerator_Callback.StateChangeSlice,
29
+ ~stripIdFromParams=false,
30
+ )
31
+
32
+ external asIdentity: 'a => Reventless.Identity.t = "%identity"
33
+
34
+ let cognito = (~userId, ~groups): Reventless.Identity.t =>
35
+ asIdentity({"userId": userId, "username": userId, "groups": groups, "provider": "Cognito"})
36
+
37
+ let iam: Reventless.Identity.t = asIdentity({
38
+ "userArn": "arn:aws:sts::1:assumed-role/Ingester",
39
+ "accountId": "1",
40
+ "username": "Ingester",
41
+ "provider": "IAM",
42
+ })
43
+
44
+ let payload = (~command, ~args: dict<JSON.t>, ~identity): CommandGenerator.payload =>
45
+ Obj.magic({
46
+ "command": command,
47
+ "arguments": Obj.magic(args),
48
+ "meta": {"ip": ["127.0.0.1"], "user": "test", "info": ""},
49
+ "identity": identity,
50
+ })
51
+
52
+ let str = JSON.Encode.string
53
+
54
+ let placeOrder = (~customerId, ~identity) =>
55
+ payload(
56
+ ~command="PlaceOrder",
57
+ ~args=Dict.fromArray([
58
+ ("orderId", str("o-1")),
59
+ ("customerId", str(customerId)),
60
+ ("note", str("gift wrap")),
61
+ ]),
62
+ ~identity,
63
+ )
64
+
65
+ let fieldOf = (cmd: Message.commandJson, name) =>
66
+ cmd.commandJson->JSON.Decode.object->Option.flatMap(o => o->Dict.get(name))
67
+
68
+ let lastPublished = () => published.contents->Array.getUnsafe(published.contents->Array.length - 1)
69
+
70
+ let _ = beforeEach(() => {
71
+ published := []
72
+ Reventless.OwnerScope.setElevatedGroups(["Admin"])
73
+ })
74
+
75
+ describe("@owner stamping on the command path:", () => {
76
+ testPromise("an omitted owner field is filled from the caller", async () => {
77
+ let _ = await generate(
78
+ payload(
79
+ ~command="PlaceOrder",
80
+ ~args=Dict.fromArray([("orderId", str("o-1")), ("note", str(""))]),
81
+ ~identity=cognito(~userId="cust-A", ~groups=["User"]),
82
+ ),
83
+ )->Effect.runPromise
84
+ expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-A")))
85
+ })
86
+
87
+ // The decisive case. An implementation that fills only the absent field passes
88
+ // every other test in this file and hands one customer another's orders.
89
+ testPromise("a FORGED owner field is overwritten, not respected", async () => {
90
+ let _ =
91
+ await generate(
92
+ placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
93
+ )->Effect.runPromise
94
+ expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-A")))
95
+ })
96
+
97
+ testPromise("non-owner fields of the same command are untouched", async () => {
98
+ let _ =
99
+ await generate(
100
+ placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
101
+ )->Effect.runPromise
102
+ expect(lastPublished()->fieldOf("note"))->toEqual(Some(str("gift wrap")))
103
+ })
104
+
105
+ testPromise("a command in the same union with no owner field is left alone", async () => {
106
+ let _ =
107
+ await generate(
108
+ payload(
109
+ ~command="ImportProducts",
110
+ ~args=Dict.fromArray([("batchId", str("b-1")), ("source", str("acme"))]),
111
+ ~identity=cognito(~userId="cust-A", ~groups=["User"]),
112
+ ),
113
+ )->Effect.runPromise
114
+ expect((lastPublished()->fieldOf("source"), lastPublished()->fieldOf("customerId")))->toEqual((
115
+ Some(str("acme")),
116
+ None,
117
+ ))
118
+ })
119
+
120
+ describe("exempt callers keep the value they sent:", () => {
121
+ // Acting on another principal's behalf must stay possible for those who may
122
+ // — otherwise an operator can never correct or back-fill anything.
123
+ testPromise("an elevated caller's chosen customerId survives", async () => {
124
+ let _ =
125
+ await generate(
126
+ placeOrder(~customerId="cust-B", ~identity=cognito(~userId="ops-1", ~groups=["Admin"])),
127
+ )->Effect.runPromise
128
+ expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-B")))
129
+ })
130
+
131
+ // The IAM shape carries no userId at all. Stamping it would write
132
+ // `undefined`; refusing it would break internal traffic. It is exempt.
133
+ testPromise("an IAM system caller's chosen customerId survives", async () => {
134
+ let _ =
135
+ await generate(placeOrder(~customerId="cust-B", ~identity=iam))->Effect.runPromise
136
+ expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-B")))
137
+ })
138
+ })
139
+
140
+ describe("a caller that cannot be identified is refused:", () => {
141
+ let refused = async p =>
142
+ switch await generate(p)->Effect.runPromise {
143
+ | _ => false
144
+ | exception _ => true
145
+ }
146
+
147
+ testPromise("an anonymous caller cannot place an owned command", async () =>
148
+ expect(
149
+ await refused(placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous)),
150
+ )->toBe(true)
151
+ )
152
+
153
+ testPromise("nothing is published when the caller is refused", async () => {
154
+ let _ = await refused(
155
+ placeOrder(~customerId="cust-B", ~identity=Reventless.Identity.anonymous),
156
+ )
157
+ expect(published.contents->Array.length)->toBe(0)
158
+ })
159
+
160
+ // The refusal is scoped to commands that actually record an owner. An
161
+ // anonymous caller invoking an unowned command has nothing to prove, and
162
+ // refusing it here would silently narrow `AllowAnonymous`.
163
+ testPromise("an anonymous caller may still issue an unowned command", async () => {
164
+ let _ = await generate(
165
+ payload(
166
+ ~command="ImportProducts",
167
+ ~args=Dict.fromArray([("batchId", str("b-1")), ("source", str("acme"))]),
168
+ ~identity=Reventless.Identity.anonymous,
169
+ ),
170
+ )->Effect.runPromise
171
+ expect(lastPublished()->fieldOf("source"))->toEqual(Some(str("acme")))
172
+ })
173
+ })
174
+ })
@@ -0,0 +1,195 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
8
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
9
+ import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
10
+ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
11
+ import * as CommandGenerator_Callback$ReventlessCore from "../../src/components/CommandGenerator/CommandGenerator_Callback.res.mjs";
12
+
13
+ S.enableJson();
14
+
15
+ let commandSchema = S.union([
16
+ S.schema(s => ({
17
+ TAG: "PlaceOrder",
18
+ orderId: s.m(DcbTag$Reventless.string),
19
+ customerId: s.m(Owner$Reventless.string),
20
+ note: s.m(S.string)
21
+ })),
22
+ S.schema(s => ({
23
+ TAG: "ImportProducts",
24
+ batchId: s.m(DcbTag$Reventless.string),
25
+ source: s.m(S.string)
26
+ }))
27
+ ]);
28
+
29
+ let published = {
30
+ contents: []
31
+ };
32
+
33
+ let generate = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(async cmds => {
34
+ published.contents = published.contents.concat(cmds);
35
+ }, undefined, "Ordering", commandSchema, "StateChangeSlice", false);
36
+
37
+ function cognito(userId, groups) {
38
+ return {
39
+ userId: userId,
40
+ username: userId,
41
+ groups: groups,
42
+ provider: "Cognito"
43
+ };
44
+ }
45
+
46
+ let iam = {
47
+ userArn: "arn:aws:sts::1:assumed-role/Ingester",
48
+ accountId: "1",
49
+ username: "Ingester",
50
+ provider: "IAM"
51
+ };
52
+
53
+ function payload(command, args, identity) {
54
+ return {
55
+ command: command,
56
+ arguments: args,
57
+ meta: {
58
+ ip: ["127.0.0.1"],
59
+ user: "test",
60
+ info: ""
61
+ },
62
+ identity: identity
63
+ };
64
+ }
65
+
66
+ function str(prim) {
67
+ return prim;
68
+ }
69
+
70
+ function placeOrder(customerId, identity) {
71
+ return payload("PlaceOrder", Object.fromEntries([
72
+ [
73
+ "orderId",
74
+ "o-1"
75
+ ],
76
+ [
77
+ "customerId",
78
+ customerId
79
+ ],
80
+ [
81
+ "note",
82
+ "gift wrap"
83
+ ]
84
+ ]), identity);
85
+ }
86
+
87
+ function fieldOf(cmd, name) {
88
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(cmd.commandJson), o => o[name]);
89
+ }
90
+
91
+ function lastPublished() {
92
+ return published.contents[published.contents.length - 1 | 0];
93
+ }
94
+
95
+ globalThis.beforeEach(() => {
96
+ published.contents = [];
97
+ OwnerScope$Reventless.setElevatedGroups(["Admin"]);
98
+ });
99
+
100
+ globalThis.describe("@owner stamping on the command path:", () => {
101
+ globalThis.test("an omitted owner field is filled from the caller", async () => {
102
+ await Effect.runPromise(generate(payload("PlaceOrder", Object.fromEntries([
103
+ [
104
+ "orderId",
105
+ "o-1"
106
+ ],
107
+ [
108
+ "note",
109
+ ""
110
+ ]
111
+ ]), cognito("cust-A", ["User"]))));
112
+ globalThis.expect(fieldOf(published.contents[published.contents.length - 1 | 0], "customerId")).toEqual("cust-A");
113
+ });
114
+ globalThis.test("a FORGED owner field is overwritten, not respected", async () => {
115
+ await Effect.runPromise(generate(placeOrder("cust-B", cognito("cust-A", ["User"]))));
116
+ globalThis.expect(fieldOf(published.contents[published.contents.length - 1 | 0], "customerId")).toEqual("cust-A");
117
+ });
118
+ globalThis.test("non-owner fields of the same command are untouched", async () => {
119
+ await Effect.runPromise(generate(placeOrder("cust-B", cognito("cust-A", ["User"]))));
120
+ globalThis.expect(fieldOf(published.contents[published.contents.length - 1 | 0], "note")).toEqual("gift wrap");
121
+ });
122
+ globalThis.test("a command in the same union with no owner field is left alone", async () => {
123
+ await Effect.runPromise(generate(payload("ImportProducts", Object.fromEntries([
124
+ [
125
+ "batchId",
126
+ "b-1"
127
+ ],
128
+ [
129
+ "source",
130
+ "acme"
131
+ ]
132
+ ]), cognito("cust-A", ["User"]))));
133
+ globalThis.expect([
134
+ fieldOf(published.contents[published.contents.length - 1 | 0], "source"),
135
+ fieldOf(published.contents[published.contents.length - 1 | 0], "customerId")
136
+ ]).toEqual([
137
+ "acme",
138
+ undefined
139
+ ]);
140
+ });
141
+ globalThis.describe("exempt callers keep the value they sent:", () => {
142
+ globalThis.test("an elevated caller's chosen customerId survives", async () => {
143
+ await Effect.runPromise(generate(placeOrder("cust-B", cognito("ops-1", ["Admin"]))));
144
+ globalThis.expect(fieldOf(published.contents[published.contents.length - 1 | 0], "customerId")).toEqual("cust-B");
145
+ });
146
+ globalThis.test("an IAM system caller's chosen customerId survives", async () => {
147
+ await Effect.runPromise(generate(placeOrder("cust-B", iam)));
148
+ globalThis.expect(fieldOf(published.contents[published.contents.length - 1 | 0], "customerId")).toEqual("cust-B");
149
+ });
150
+ });
151
+ globalThis.describe("a caller that cannot be identified is refused:", () => {
152
+ let refused = async p => {
153
+ try {
154
+ await Effect.runPromise(generate(p));
155
+ return false;
156
+ } catch (exn) {
157
+ return true;
158
+ }
159
+ };
160
+ globalThis.test("an anonymous caller cannot place an owned command", async () => {
161
+ globalThis.expect(await refused(placeOrder("cust-B", Identity$Reventless.anonymous))).toBe(true);
162
+ });
163
+ globalThis.test("nothing is published when the caller is refused", async () => {
164
+ await refused(placeOrder("cust-B", Identity$Reventless.anonymous));
165
+ globalThis.expect(published.contents.length).toBe(0);
166
+ });
167
+ globalThis.test("an anonymous caller may still issue an unowned command", async () => {
168
+ await Effect.runPromise(generate(payload("ImportProducts", Object.fromEntries([
169
+ [
170
+ "batchId",
171
+ "b-1"
172
+ ],
173
+ [
174
+ "source",
175
+ "acme"
176
+ ]
177
+ ]), Identity$Reventless.anonymous)));
178
+ globalThis.expect(fieldOf(published.contents[published.contents.length - 1 | 0], "source")).toEqual("acme");
179
+ });
180
+ });
181
+ });
182
+
183
+ export {
184
+ commandSchema,
185
+ published,
186
+ generate,
187
+ cognito,
188
+ iam,
189
+ payload,
190
+ str,
191
+ placeOrder,
192
+ fieldOf,
193
+ lastPublished,
194
+ }
195
+ /* Not a pure module */
@@ -0,0 +1,110 @@
1
+ // `requiredAccess` is derived from the authorization rule the server enforces, so
2
+ // what these assert is that the manifest cannot describe a different policy from
3
+ // the one the resolver applies. A client uses the keys to stop offering what it
4
+ // would be refused; it never decides anything with them.
5
+
6
+ open JestGlobals
7
+
8
+ type scsComponent = Component.t<
9
+ ReventlessInfra.StateChangeSlice.t,
10
+ ReventlessInfra.StateChangeSlice.outputs,
11
+ ReventlessInfra.StateChangeSlice.operations,
12
+ >
13
+ type svsComponent = Component.t<
14
+ ReventlessInfra.StateViewSlice.t,
15
+ ReventlessInfra.StateViewSlice.outputs,
16
+ ReventlessInfra.StateViewSlice.operations,
17
+ >
18
+
19
+ module PsGatedCommandsSlice: ReventlessInfra.StateChangeSlice.T = {
20
+ module Spec = PsGatedCommands
21
+ module Behavior = {
22
+ type state = PsGatedCommands.state
23
+ let initialState = PsGatedCommands.initialState
24
+ let evolve = PsGatedCommands.evolve
25
+ let decide = PsGatedCommands.decide
26
+ let moduleUrl = PsGatedCommands.moduleUrl
27
+ }
28
+ let isAsync = false
29
+ type component = scsComponent
30
+ let make = (
31
+ ~dcbEventLog as _,
32
+ ~publishJsons as _,
33
+ ~tagKeysByEventType as _=?,
34
+ ~crossPartitionTagKeys as _=?,
35
+ ~runtime as _=?,
36
+ ~opts as _=?,
37
+ ): component => Obj.magic(0)
38
+ }
39
+
40
+ module PsGatedViewSlice: ReventlessInfra.StateViewSlice.T = {
41
+ module Spec = PsGatedView
42
+ module Projection = {
43
+ let project = PsGatedView.project
44
+ let moduleUrl = PsGatedView.moduleUrl
45
+ }
46
+ type component = svsComponent
47
+ let make = (~dcbEventLog as _, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
48
+ }
49
+
50
+ module PsOrdersViewSlice: ReventlessInfra.StateViewSlice.T = {
51
+ module Spec = PsOrdersView
52
+ module Projection = {
53
+ let project = PsOrdersView.project
54
+ let moduleUrl = PsOrdersView.moduleUrl
55
+ }
56
+ type component = svsComponent
57
+ let make = (~dcbEventLog as _, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
58
+ }
59
+
60
+ let structure = Plugin_Structure.make(
61
+ ~name="AccessPlugin",
62
+ ~stateChangeSlices=[module(PsGatedCommandsSlice)],
63
+ ~stateViewSlices=[module(PsGatedViewSlice), module(PsOrdersViewSlice)],
64
+ )
65
+
66
+ let commandNamed = name =>
67
+ structure.stateChangeSlices
68
+ ->Array.flatMap(w => w.commands)
69
+ ->Array.find(c => c.name === name)
70
+ ->Option.flatMap(c => c.requiredAccess)
71
+
72
+ let viewNamed = name =>
73
+ structure.stateViewSlices
74
+ ->Array.find(q => q.name === name)
75
+ ->Option.flatMap(q => q.requiredAccess)
76
+
77
+ describe("requiredAccess derived from the authorization rule", () => {
78
+ testSync("an AllowGroups command publishes its groups", () =>
79
+ expect(commandNamed("Restock"))->toEqual(Some(["Admin", "Ops"]))
80
+ )
81
+
82
+ // The reason the rule is evaluated per constructor rather than per component:
83
+ // one slice, two audiences, and a component-level shortcut would gate both.
84
+ testSync("a sibling command in the same slice publishes nothing", () =>
85
+ expect(commandNamed("RequestRestock"))->toEqual(None)
86
+ )
87
+
88
+ testSync("a module-level rule reaches the view it guards", () =>
89
+ expect(viewNamed("GatedView"))->toEqual(Some(["Admin"]))
90
+ )
91
+
92
+ testSync("an unannotated view publishes nothing", () =>
93
+ expect(viewNamed("Orders"))->toEqual(None)
94
+ )
95
+
96
+ // Null rather than `[]` on the wire: "no key gates this" is one answer, and a
97
+ // def written before the field existed has to read as the same thing.
98
+ testSync("nothing to require encodes as null, not an empty list", () => {
99
+ let json =
100
+ Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
101
+ ~pluginId="AccessPlugin",
102
+ structure,
103
+ )->JSON.stringify
104
+ expect((
105
+ json->String.includes(`"requiredAccess":null`),
106
+ json->String.includes(`"requiredAccess":["Admin","Ops"]`),
107
+ json->String.includes(`"requiredAccess":[]`),
108
+ ))->toEqual((true, true, false))
109
+ })
110
+ })
@@ -0,0 +1,144 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as PsGatedView$ReventlessCore from "./StateViewSlice/PsGatedView.res.mjs";
6
+ import * as PsOrdersView$ReventlessCore from "./StateViewSlice/PsOrdersView.res.mjs";
7
+ import * as PsGatedCommands$ReventlessCore from "./StateChangeSlice/PsGatedCommands.res.mjs";
8
+ import * as Plugin_Structure$ReventlessCore from "../../src/plugin/component/Plugin_Structure.res.mjs";
9
+ import * as Platform_ComponentDefinitionsApi$ReventlessCore from "../../src/admin/Platform_ComponentDefinitionsApi.res.mjs";
10
+
11
+ let Behavior = {
12
+ initialState: false,
13
+ evolve: PsGatedCommands$ReventlessCore.evolve,
14
+ decide: PsGatedCommands$ReventlessCore.decide,
15
+ moduleUrl: PsGatedCommands$ReventlessCore.moduleUrl
16
+ };
17
+
18
+ function make(param, param$1, param$2, param$3, param$4, param$5) {
19
+ return 0;
20
+ }
21
+
22
+ let PsGatedCommandsSlice_Spec = {
23
+ name: PsGatedCommands$ReventlessCore.name,
24
+ moduleUrl: PsGatedCommands$ReventlessCore.moduleUrl,
25
+ Id: Id$Reventless.$$String,
26
+ consumedEventSchema: PsGatedCommands$ReventlessCore.consumedEventSchema,
27
+ errorSchema: PsGatedCommands$ReventlessCore.errorSchema,
28
+ eventSchema: PsGatedCommands$ReventlessCore.eventSchema,
29
+ commandSchema: PsGatedCommands$ReventlessCore.commandSchema,
30
+ commandAuthorization: PsGatedCommands$ReventlessCore.commandAuthorization,
31
+ readConsistency: PsGatedCommands$ReventlessCore.readConsistency
32
+ };
33
+
34
+ let PsGatedCommandsSlice = {
35
+ Spec: PsGatedCommandsSlice_Spec,
36
+ Behavior: Behavior,
37
+ isAsync: false,
38
+ make: make
39
+ };
40
+
41
+ let Projection = {
42
+ project: PsGatedView$ReventlessCore.project,
43
+ moduleUrl: PsGatedView$ReventlessCore.moduleUrl
44
+ };
45
+
46
+ function make$1(param, param$1, param$2) {
47
+ return 0;
48
+ }
49
+
50
+ let PsGatedViewSlice_Spec = {
51
+ name: PsGatedView$ReventlessCore.name,
52
+ moduleUrl: PsGatedView$ReventlessCore.moduleUrl,
53
+ stateSchema: PsGatedView$ReventlessCore.stateSchema,
54
+ consumedEventSchema: PsGatedView$ReventlessCore.consumedEventSchema,
55
+ config: PsGatedView$ReventlessCore.config,
56
+ subIdConfig: undefined,
57
+ authorization: PsGatedView$ReventlessCore.authorization,
58
+ visibility: PsGatedView$ReventlessCore.visibility
59
+ };
60
+
61
+ let PsGatedViewSlice = {
62
+ Spec: PsGatedViewSlice_Spec,
63
+ Projection: Projection,
64
+ make: make$1
65
+ };
66
+
67
+ let Projection$1 = {
68
+ project: PsOrdersView$ReventlessCore.project,
69
+ moduleUrl: PsOrdersView$ReventlessCore.moduleUrl
70
+ };
71
+
72
+ function make$2(param, param$1, param$2) {
73
+ return 0;
74
+ }
75
+
76
+ let PsOrdersViewSlice_Spec = {
77
+ name: PsOrdersView$ReventlessCore.name,
78
+ moduleUrl: PsOrdersView$ReventlessCore.moduleUrl,
79
+ stateSchema: PsOrdersView$ReventlessCore.stateSchema,
80
+ consumedEventSchema: PsOrdersView$ReventlessCore.consumedEventSchema,
81
+ config: PsOrdersView$ReventlessCore.config,
82
+ subIdConfig: undefined,
83
+ authorization: PsOrdersView$ReventlessCore.authorization,
84
+ visibility: PsOrdersView$ReventlessCore.visibility
85
+ };
86
+
87
+ let PsOrdersViewSlice = {
88
+ Spec: PsOrdersViewSlice_Spec,
89
+ Projection: Projection$1,
90
+ make: make$2
91
+ };
92
+
93
+ let structure = Plugin_Structure$ReventlessCore.make("AccessPlugin", undefined, undefined, [
94
+ PsGatedViewSlice,
95
+ PsOrdersViewSlice
96
+ ], [PsGatedCommandsSlice], undefined, undefined, undefined, undefined, undefined, undefined);
97
+
98
+ function commandNamed(name) {
99
+ return Stdlib_Option.flatMap(structure.stateChangeSlices.flatMap(w => w.commands).find(c => c.name === name), c => c.requiredAccess);
100
+ }
101
+
102
+ function viewNamed(name) {
103
+ return Stdlib_Option.flatMap(structure.stateViewSlices.find(q => q.name === name), q => q.requiredAccess);
104
+ }
105
+
106
+ globalThis.describe("requiredAccess derived from the authorization rule", () => {
107
+ globalThis.test("an AllowGroups command publishes its groups", () => {
108
+ globalThis.expect(commandNamed("Restock")).toEqual([
109
+ "Admin",
110
+ "Ops"
111
+ ]);
112
+ });
113
+ globalThis.test("a sibling command in the same slice publishes nothing", () => {
114
+ globalThis.expect(commandNamed("RequestRestock")).toEqual(undefined);
115
+ });
116
+ globalThis.test("a module-level rule reaches the view it guards", () => {
117
+ globalThis.expect(viewNamed("GatedView")).toEqual(["Admin"]);
118
+ });
119
+ globalThis.test("an unannotated view publishes nothing", () => {
120
+ globalThis.expect(viewNamed("Orders")).toEqual(undefined);
121
+ });
122
+ globalThis.test("nothing to require encodes as null, not an empty list", () => {
123
+ let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("AccessPlugin", structure));
124
+ globalThis.expect([
125
+ json.includes(`"requiredAccess":null`),
126
+ json.includes(`"requiredAccess":["Admin","Ops"]`),
127
+ json.includes(`"requiredAccess":[]`)
128
+ ]).toEqual([
129
+ true,
130
+ true,
131
+ false
132
+ ]);
133
+ });
134
+ });
135
+
136
+ export {
137
+ PsGatedCommandsSlice,
138
+ PsGatedViewSlice,
139
+ PsOrdersViewSlice,
140
+ structure,
141
+ commandNamed,
142
+ viewNamed,
143
+ }
144
+ /* structure Not a pure module */