@reventlessdev/reventless-core 3.0.0-alpha.224 → 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.
- package/CHANGELOG.md +14 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin_Structure.res +6 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +9 -3
- package/src/admin/Platform_BakedManifest.res +154 -0
- package/src/admin/Platform_BakedManifest.res.mjs +156 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +9 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +18 -2
- package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
- package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
- package/src/components/Api/QueryDbListQuery.res +25 -1
- package/src/components/Api/QueryDbListQuery.res.mjs +10 -3
- package/src/components/Api/SuryToJsonSchema.res +19 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +18 -4
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +48 -0
- package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +27 -1
- package/src/plugin/component/Plugin_Structure.res +59 -3
- package/src/plugin/component/Plugin_Structure.res.mjs +29 -9
- package/tests/admin/Platform_BakedManifestTest.res +244 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +10 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +24 -8
- package/tests/admin/Platform_PluginStructuresApiTest.res +2 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +6 -2
- package/tests/api/Api_IdsTest.res.mjs +1 -1
- package/tests/api/OwnerScopeDiagnosticsTest.res +109 -0
- package/tests/api/OwnerScopeDiagnosticsTest.res.mjs +91 -0
- package/tests/api/SuryToJsonSchemaTest.res +45 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +23 -0
- package/tests/commandgenerator/OwnerStampingTest.res +174 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +195 -0
- package/tests/plugin/PluginStructureAccessTest.res +110 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +144 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
- package/tests/plugin/StateViewSlice/PsGatedView.res +15 -0
- package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +60 -0
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +4 -0
|
@@ -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 */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Test fixture spec: one slice carrying commands with different audiences.
|
|
2
|
+
// `Restock` is operator-only, `RequestRestock` is open to anyone signed in —
|
|
3
|
+
// which is the case a component-level access rule would get wrong.
|
|
4
|
+
|
|
5
|
+
@@reventless.spec("GatedCommands")
|
|
6
|
+
|
|
7
|
+
type state = bool
|
|
8
|
+
let initialState = false
|
|
9
|
+
|
|
10
|
+
@schema
|
|
11
|
+
type consumedEvent = StockLow({productId: string})
|
|
12
|
+
|
|
13
|
+
let evolve = (_state, _event) => true
|
|
14
|
+
|
|
15
|
+
@schema
|
|
16
|
+
type command =
|
|
17
|
+
| @authorize(AllowGroups(["Admin", "Ops"])) Restock({productId: string})
|
|
18
|
+
| RequestRestock({productId: string})
|
|
19
|
+
|
|
20
|
+
@schema
|
|
21
|
+
type error = UnknownProduct
|
|
22
|
+
|
|
23
|
+
@schema
|
|
24
|
+
type event =
|
|
25
|
+
| Restocked({productId: string})
|
|
26
|
+
| RestockRequested({productId: string})
|
|
27
|
+
|
|
28
|
+
let decide = (_state, _command): result<array<event>, error> => Ok([])
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
5
|
+
|
|
6
|
+
let consumedEventSchema = S.schema(s => ({
|
|
7
|
+
TAG: "StockLow",
|
|
8
|
+
productId: s.m(DcbTag$Reventless.string)
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
function evolve(_state, _event) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let commandSchema = S.union([
|
|
16
|
+
S.schema(s => ({
|
|
17
|
+
TAG: "Restock",
|
|
18
|
+
productId: s.m(DcbTag$Reventless.string)
|
|
19
|
+
})),
|
|
20
|
+
S.schema(s => ({
|
|
21
|
+
TAG: "RequestRestock",
|
|
22
|
+
productId: s.m(DcbTag$Reventless.string)
|
|
23
|
+
}))
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
let errorSchema = S.literal("UnknownProduct");
|
|
27
|
+
|
|
28
|
+
let eventSchema = S.union([
|
|
29
|
+
S.schema(s => ({
|
|
30
|
+
TAG: "Restocked",
|
|
31
|
+
productId: s.m(DcbTag$Reventless.string)
|
|
32
|
+
})),
|
|
33
|
+
S.schema(s => ({
|
|
34
|
+
TAG: "RestockRequested",
|
|
35
|
+
productId: s.m(DcbTag$Reventless.string)
|
|
36
|
+
}))
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
function decide(_state, _command) {
|
|
40
|
+
return {
|
|
41
|
+
TAG: "Ok",
|
|
42
|
+
_0: []
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function commandAuthorization(command) {
|
|
47
|
+
if (command.TAG === "Restock") {
|
|
48
|
+
return {
|
|
49
|
+
TAG: "AllowGroups",
|
|
50
|
+
_0: [
|
|
51
|
+
"Admin",
|
|
52
|
+
"Ops"
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
} else {
|
|
56
|
+
return "AllowAuthenticated";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let name = "GatedCommands";
|
|
61
|
+
|
|
62
|
+
let Id;
|
|
63
|
+
|
|
64
|
+
let initialState = false;
|
|
65
|
+
|
|
66
|
+
let moduleUrl = "@reventlessdev/reventless-core/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs";
|
|
67
|
+
|
|
68
|
+
let readConsistency = "EscalateOnRetry";
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
name,
|
|
72
|
+
Id,
|
|
73
|
+
initialState,
|
|
74
|
+
consumedEventSchema,
|
|
75
|
+
evolve,
|
|
76
|
+
commandSchema,
|
|
77
|
+
errorSchema,
|
|
78
|
+
eventSchema,
|
|
79
|
+
decide,
|
|
80
|
+
moduleUrl,
|
|
81
|
+
commandAuthorization,
|
|
82
|
+
readConsistency,
|
|
83
|
+
}
|
|
84
|
+
/* consumedEventSchema Not a pure module */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Test fixture spec: a view whose module-level rule restricts who may read it.
|
|
2
|
+
|
|
3
|
+
@@reventless.spec("GatedView")
|
|
4
|
+
@@reventless.authorize(AllowGroups(["Admin"]))
|
|
5
|
+
|
|
6
|
+
@schema
|
|
7
|
+
type consumedEvent = OrderPlaced({orderId: string, customerName: string})
|
|
8
|
+
|
|
9
|
+
@schema
|
|
10
|
+
type state = {orderId: string, customerName: string}
|
|
11
|
+
|
|
12
|
+
let project = ({event}: Reventless.StateViewSlice.consumed<consumedEvent>) =>
|
|
13
|
+
switch event {
|
|
14
|
+
| OrderPlaced({orderId, customerName}) => [Set(orderId, {orderId, customerName})]
|
|
15
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
5
|
+
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
6
|
+
|
|
7
|
+
let consumedEventSchema = S.schema(s => ({
|
|
8
|
+
TAG: "OrderPlaced",
|
|
9
|
+
orderId: s.m(DcbTag$Reventless.string),
|
|
10
|
+
customerName: s.m(S.string)
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
let stateSchema = S.schema(s => ({
|
|
14
|
+
orderId: s.m(S.string),
|
|
15
|
+
customerName: s.m(S.string)
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
function project(param) {
|
|
19
|
+
let event = param.event;
|
|
20
|
+
let orderId = event.orderId;
|
|
21
|
+
return [{
|
|
22
|
+
TAG: "Set",
|
|
23
|
+
_0: orderId,
|
|
24
|
+
_1: {
|
|
25
|
+
orderId: orderId,
|
|
26
|
+
customerName: event.customerName
|
|
27
|
+
}
|
|
28
|
+
}];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let config = ReadModel$Reventless.config(undefined, undefined, undefined);
|
|
32
|
+
|
|
33
|
+
let authorization = {
|
|
34
|
+
TAG: "AllowGroups",
|
|
35
|
+
_0: ["Admin"]
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let name = "GatedView";
|
|
39
|
+
|
|
40
|
+
let Id;
|
|
41
|
+
|
|
42
|
+
let subIdConfig;
|
|
43
|
+
|
|
44
|
+
let moduleUrl = "@reventlessdev/reventless-core/tests/plugin/StateViewSlice/PsGatedView.res.mjs";
|
|
45
|
+
|
|
46
|
+
let visibility = "Public";
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
name,
|
|
50
|
+
Id,
|
|
51
|
+
consumedEventSchema,
|
|
52
|
+
stateSchema,
|
|
53
|
+
project,
|
|
54
|
+
config,
|
|
55
|
+
subIdConfig,
|
|
56
|
+
moduleUrl,
|
|
57
|
+
authorization,
|
|
58
|
+
visibility,
|
|
59
|
+
}
|
|
60
|
+
/* consumedEventSchema Not a pure module */
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
.structure.aggregates[].commands[].name: string
|
|
42
42
|
.structure.aggregates[].commands[].references[].entity: string
|
|
43
43
|
.structure.aggregates[].commands[].references[].fieldName: string
|
|
44
|
+
.structure.aggregates[].commands[].requiredAccess[]: string
|
|
44
45
|
.structure.aggregates[].commands[].schema: string
|
|
45
46
|
.structure.aggregates[].consumedEventTypes[]: string
|
|
46
47
|
.structure.aggregates[].errors[].name: string
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
.structure.readModels[].linkedWriteSide[]: string
|
|
78
79
|
.structure.readModels[].name: string
|
|
79
80
|
.structure.readModels[].queryField: string
|
|
81
|
+
.structure.readModels[].requiredAccess[]: string
|
|
80
82
|
.structure.readModels[].schema: string
|
|
81
83
|
.structure.readModels[].searchableFields[]: string
|
|
82
84
|
.structure.requiredStoreDeclarations[].component: string
|
|
@@ -88,6 +90,7 @@
|
|
|
88
90
|
.structure.stateChangeSlices[].commands[].name: string
|
|
89
91
|
.structure.stateChangeSlices[].commands[].references[].entity: string
|
|
90
92
|
.structure.stateChangeSlices[].commands[].references[].fieldName: string
|
|
93
|
+
.structure.stateChangeSlices[].commands[].requiredAccess[]: string
|
|
91
94
|
.structure.stateChangeSlices[].commands[].schema: string
|
|
92
95
|
.structure.stateChangeSlices[].consumedEventTypes[]: string
|
|
93
96
|
.structure.stateChangeSlices[].errors[].name: string
|
|
@@ -106,6 +109,7 @@
|
|
|
106
109
|
.structure.stateViewSlices[].linkedWriteSide[]: string
|
|
107
110
|
.structure.stateViewSlices[].name: string
|
|
108
111
|
.structure.stateViewSlices[].queryField: string
|
|
112
|
+
.structure.stateViewSlices[].requiredAccess[]: string
|
|
109
113
|
.structure.stateViewSlices[].schema: string
|
|
110
114
|
.structure.stateViewSlices[].searchableFields[]: string
|
|
111
115
|
.version: string
|