@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.
- package/CHANGELOG.md +36 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin_Structure.res +16 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +14 -3
- package/src/admin/Platform_BakedManifest.res +154 -0
- package/src/admin/Platform_BakedManifest.res.mjs +156 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +19 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +30 -2
- package/src/components/Api/Api_Ids.res +63 -0
- package/src/components/Api/Api_Ids.res.mjs +43 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res +121 -28
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +97 -27
- package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
- package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
- package/src/components/Api/QueryDbListQuery.res +38 -6
- package/src/components/Api/QueryDbListQuery.res.mjs +14 -5
- package/src/components/Api/SchemaType.res +36 -2
- package/src/components/Api/SchemaType.res.mjs +31 -10
- 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 +78 -3
- package/src/plugin/component/Plugin_Structure.res.mjs +38 -9
- package/tests/admin/Platform_BakedManifestTest.res +244 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +73 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +135 -6
- package/tests/admin/Platform_PluginStructuresApiTest.res +12 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +27 -2
- package/tests/api/Api_IdsTest.res +143 -0
- package/tests/api/Api_IdsTest.res.mjs +97 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res +169 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +179 -0
- 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/PluginStructureTest.res +90 -0
- package/tests/plugin/PluginStructureTest.res.mjs +86 -8
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
- package/tests/plugin/StateViewSlice/PsCategoriesView.res +17 -0
- package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +57 -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,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 */
|