@reventlessdev/reventless-core 3.0.0-alpha.296 → 3.0.0-alpha.298
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 +25 -0
- package/package.json +9 -9
- package/src/components/Api/GraphQL_FragmentGenerator.res +15 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +13 -0
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +2 -2
- package/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs +5 -2
- package/src/components/Dcb/Dcb_Builder.res +39 -12
- package/src/components/Dcb/Dcb_Builder.res.mjs +18 -8
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +8 -3
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res.mjs +10 -3
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res +45 -10
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +48 -23
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Webhook.res +231 -0
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Webhook.res.mjs +294 -0
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +1 -1
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res.mjs +3 -1
- package/tests/plugin/PluginStructureAccessTest.res +1 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +19 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.298 (2026-10-16)
|
|
7
|
+
|
|
8
|
+
* feat!(inbound): a translation slice can declare a signed webhook URL ([0ad6994](https://github.com/ReventlessDev/reventless-core/commit/0ad69949fcc91c2756c60ca8cb120dd468841d8b))
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* InboundTranslationSlice.Spec requires `endpoint`. Specs under
|
|
13
|
+
@@reventless.spec get it from the PPX (republish reventless-ppx with this
|
|
14
|
+
release); hand-written specs add `let endpoint = None`.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 3.0.0-alpha.297 (2026-10-16)
|
|
19
|
+
|
|
20
|
+
* fix(auth)!: only operators read the framework's own stores ([a2fd202](https://github.com/ReventlessDev/reventless-core/commit/a2fd2022b6876dae855b185df7f8263353fc971d))
|
|
21
|
+
|
|
22
|
+
### BREAKING CHANGES
|
|
23
|
+
|
|
24
|
+
* a caller outside the elevated groups is refused the
|
|
25
|
+
framework's to-do, schedule and audit stores. On AWS, a roles-gated
|
|
26
|
+
view's …ByIds, …Refs, …Items and …By<Index> fields refuse callers
|
|
27
|
+
outside its roles.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
6
31
|
# 3.0.0-alpha.296 (2026-10-16)
|
|
7
32
|
|
|
8
33
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.298",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,21 +29,21 @@
|
|
|
29
29
|
"sury": "11.0.0",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "1.0.0-alpha.0",
|
|
32
|
-
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.22",
|
|
33
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.12",
|
|
34
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
35
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.15",
|
|
36
|
-
"@reventlessdev/rescript-
|
|
33
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
34
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.12",
|
|
35
|
+
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.22",
|
|
36
|
+
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
37
37
|
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.23",
|
|
38
38
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
|
|
39
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-
|
|
39
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.41",
|
|
40
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.195",
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.167"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.1",
|
|
45
45
|
"sury-ppx": "11.0.0",
|
|
46
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
46
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.102"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"rescript": "^12.3.1"
|
|
@@ -777,6 +777,21 @@ let mutationArgTypes = (~fieldName: string, variantSchema: S.t<unknown>): option
|
|
|
777
777
|
out
|
|
778
778
|
})
|
|
779
779
|
|
|
780
|
+
/** Every query field `generate` emits for an entry, under the same conditions.
|
|
781
|
+
A platform gating an entry gates each of them: a door left out is open. */
|
|
782
|
+
let queryFieldNamesOf = (entry: querySchemaEntry): array<string> => {
|
|
783
|
+
let byIds = entry.includeIdParam->Option.getOr(true) && entry.subIdField === None
|
|
784
|
+
let indexFields =
|
|
785
|
+
entry.indexQueries
|
|
786
|
+
->Option.getOr([])
|
|
787
|
+
->Array.filter(ic => !Reventless.ReadModel.isDerivedIndex(ic))
|
|
788
|
+
->Array.map(ic => indexQueryFieldName(~singleFieldName=entry.singleFieldName, ~index=ic.index))
|
|
789
|
+
[entry.singleFieldName, entry.listFieldName]
|
|
790
|
+
->Array.concat(byIds ? [entry.listFieldName ++ "ByIds", entry.listFieldName ++ "Refs"] : [])
|
|
791
|
+
->Array.concat(entry.subIdField->Option.isSome ? [entry.singleFieldName ++ "Items"] : [])
|
|
792
|
+
->Array.concat(indexFields)
|
|
793
|
+
}
|
|
794
|
+
|
|
780
795
|
// ── Main generate function ─────────────────────────────────────────────────
|
|
781
796
|
|
|
782
797
|
let generate = (
|
|
@@ -529,6 +529,18 @@ function mutationArgTypes(fieldName, variantSchema) {
|
|
|
529
529
|
});
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
function queryFieldNamesOf(entry) {
|
|
533
|
+
let byIds = Stdlib_Option.getOr(entry.includeIdParam, true) && entry.subIdField === undefined;
|
|
534
|
+
let indexFields = Stdlib_Option.getOr(entry.indexQueries, []).filter(ic => !ReadModel$Reventless.isDerivedIndex(ic)).map(ic => indexQueryFieldName(entry.singleFieldName, ic.index));
|
|
535
|
+
return [
|
|
536
|
+
entry.singleFieldName,
|
|
537
|
+
entry.listFieldName
|
|
538
|
+
].concat(byIds ? [
|
|
539
|
+
entry.listFieldName + "ByIds",
|
|
540
|
+
entry.listFieldName + "Refs"
|
|
541
|
+
] : []).concat(Stdlib_Option.isSome(entry.subIdField) ? [entry.singleFieldName + "Items"] : []).concat(indexFields);
|
|
542
|
+
}
|
|
543
|
+
|
|
532
544
|
function generate(mutationEntries, queryEntries) {
|
|
533
545
|
let types = [];
|
|
534
546
|
let mutations = [];
|
|
@@ -729,6 +741,7 @@ export {
|
|
|
729
741
|
deriveIndexQueryField,
|
|
730
742
|
deriveMutationFieldFromObject,
|
|
731
743
|
mutationArgTypes,
|
|
744
|
+
queryFieldNamesOf,
|
|
732
745
|
generate,
|
|
733
746
|
}
|
|
734
747
|
/* log Not a pure module */
|
|
@@ -63,7 +63,7 @@ module Make = (
|
|
|
63
63
|
let config = Reventless.ReadModel.config()
|
|
64
64
|
let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>> = None
|
|
65
65
|
type role = Reventless.Role.name
|
|
66
|
-
let authorization: Reventless.Authorization.rule<role> =
|
|
66
|
+
let authorization: Reventless.Authorization.rule<role> = Reventless.OwnerScope.operators()
|
|
67
67
|
let visibility: Reventless.Visibility.t = Public
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -81,7 +81,7 @@ module Make = (
|
|
|
81
81
|
let config = Reventless.ReadModel.config()
|
|
82
82
|
let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>> = None
|
|
83
83
|
type role = Reventless.Role.name
|
|
84
|
-
let authorization: Reventless.Authorization.rule<role> =
|
|
84
|
+
let authorization: Reventless.Authorization.rule<role> = Reventless.OwnerScope.operators()
|
|
85
85
|
let visibility: Reventless.Visibility.t = Public
|
|
86
86
|
}
|
|
87
87
|
module SchedulesQueryDb = QueryDb_Builder.Make(
|
|
@@ -15,6 +15,7 @@ import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components
|
|
|
15
15
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
16
16
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
17
17
|
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
18
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
18
19
|
import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
19
20
|
import * as EventTopic$ReventlessCore from "../EventTopic/EventTopic.res.mjs";
|
|
20
21
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
@@ -36,6 +37,7 @@ function Make(RuntimeEnvironment) {
|
|
|
36
37
|
let readsClock = mappedSources.includes(Clock$Reventless.name);
|
|
37
38
|
let moduleUrl = import.meta.url;
|
|
38
39
|
let config = ReadModel$Reventless.config(undefined, undefined, undefined);
|
|
40
|
+
let authorization = OwnerScope$Reventless.operators();
|
|
39
41
|
let SpecificQueryDb = QueryDb_Builder$ReventlessCore.Make({
|
|
40
42
|
Id: Id$Reventless.$$String,
|
|
41
43
|
name: queryDbName,
|
|
@@ -43,13 +45,14 @@ function Make(RuntimeEnvironment) {
|
|
|
43
45
|
stateSchema: AutomationSlice_Callback$ReventlessCore.todoRowSchema,
|
|
44
46
|
config: config,
|
|
45
47
|
subIdConfig: undefined,
|
|
46
|
-
authorization:
|
|
48
|
+
authorization: authorization,
|
|
47
49
|
visibility: "Public"
|
|
48
50
|
})(QueryDbStorage)(QueryDbResolvers);
|
|
49
51
|
let schedulesStoreName = Spec.name + "Schedule";
|
|
50
52
|
let schedulesDbName = readsClock ? schedulesStoreName : undefined;
|
|
51
53
|
let moduleUrl$1 = import.meta.url;
|
|
52
54
|
let config$1 = ReadModel$Reventless.config(undefined, undefined, undefined);
|
|
55
|
+
let authorization$1 = OwnerScope$Reventless.operators();
|
|
53
56
|
let SchedulesQueryDb = QueryDb_Builder$ReventlessCore.Make({
|
|
54
57
|
Id: Id$Reventless.$$String,
|
|
55
58
|
name: schedulesStoreName,
|
|
@@ -57,7 +60,7 @@ function Make(RuntimeEnvironment) {
|
|
|
57
60
|
stateSchema: AutomationSlice_Schedules$ReventlessCore.rowSchema,
|
|
58
61
|
config: config$1,
|
|
59
62
|
subIdConfig: undefined,
|
|
60
|
-
authorization:
|
|
63
|
+
authorization: authorization$1,
|
|
61
64
|
visibility: "Public"
|
|
62
65
|
})(QueryDbStorage)(QueryDbResolvers);
|
|
63
66
|
let syncSchedules = async (ops, stored) => {
|
|
@@ -759,38 +759,51 @@ module Make = (
|
|
|
759
759
|
~opts,
|
|
760
760
|
)
|
|
761
761
|
let fieldName = Api_Naming.sliceMutationField(~plugin=name, ~slice=ITS.Spec.name)
|
|
762
|
+
// `@noApi` on `externalInput` withholds the mutation; the webhook is
|
|
763
|
+
// then the only door, and a slice with neither could never be reached.
|
|
764
|
+
let exposed = InboundTranslationSlice_Callback.exposesMutation(
|
|
765
|
+
ITS.Spec.externalInputSchema->S.castToUnknown,
|
|
766
|
+
)
|
|
767
|
+
InboundTranslationSlice_Callback.checkReachable(
|
|
768
|
+
~plugin=name,
|
|
769
|
+
~slice=ITS.Spec.name,
|
|
770
|
+
~mutation=exposed,
|
|
771
|
+
~webhook=ITS.Spec.endpoint->Option.isSome,
|
|
772
|
+
)
|
|
762
773
|
|
|
763
774
|
switch HooksConfig.hooks.inboundMutationResolverHook {
|
|
764
|
-
| Some(registerResolver) =>
|
|
775
|
+
| Some(registerResolver) if exposed =>
|
|
765
776
|
registerResolver(
|
|
766
777
|
~fieldName,
|
|
767
778
|
~externalInputSchema=ITS.Spec.externalInputSchema->S.castToUnknown,
|
|
768
779
|
~permission=?inboundDoorPermission(ITS.Spec.commandSchema, ITS.Spec.authorizationOf),
|
|
769
780
|
)
|
|
770
|
-
|
|
|
781
|
+
| _ => ()
|
|
771
782
|
}
|
|
772
783
|
|
|
773
784
|
switch HooksConfig.hooks.inboundMutationBindReceiveHook {
|
|
774
|
-
| Some(bindReceive) =>
|
|
785
|
+
| Some(bindReceive) if exposed =>
|
|
775
786
|
let _ =
|
|
776
787
|
its
|
|
777
788
|
->Component.operations
|
|
778
|
-
->Pulumi.Output.apply(ops =>
|
|
779
|
-
|
|
789
|
+
->Pulumi.Output.apply(ops =>
|
|
790
|
+
bindReceive(~fieldName, ~receive=(input, ~caller=?) => ops.receive(input, ~caller?))
|
|
791
|
+
)
|
|
792
|
+
| _ => ()
|
|
780
793
|
}
|
|
781
794
|
|
|
782
|
-
(ITS.Spec.name, fieldName, its, ITS.Spec.externalInputSchema->S.castToUnknown)
|
|
795
|
+
(ITS.Spec.name, fieldName, its, ITS.Spec.externalInputSchema->S.castToUnknown, exposed)
|
|
783
796
|
})
|
|
784
797
|
|
|
785
798
|
let inboundTranslationSlicesOutputs =
|
|
786
799
|
inboundTranslationSliceData
|
|
787
|
-
->Array.map(((specName, _, its, _)) => (specName, its->Component.outputs))
|
|
800
|
+
->Array.map(((specName, _, its, _, _)) => (specName, its->Component.outputs))
|
|
788
801
|
->Dict.fromArray
|
|
789
802
|
|
|
790
803
|
// Collect InboundTranslationSlice receive functions for composite handler routing
|
|
791
804
|
let inboundReceiversOutput =
|
|
792
805
|
inboundTranslationSliceData
|
|
793
|
-
->Array.map(((_, fieldName, its, _)) =>
|
|
806
|
+
->Array.map(((_, fieldName, its, _, _)) =>
|
|
794
807
|
its
|
|
795
808
|
->Component.operations
|
|
796
809
|
->Pulumi.Output.apply(ops => (fieldName, ops.receive))
|
|
@@ -942,9 +955,10 @@ module Make = (
|
|
|
942
955
|
->Dict.valuesToArray
|
|
943
956
|
->Array.flatMap(outputs => outputs.resources)
|
|
944
957
|
|
|
945
|
-
let
|
|
946
|
-
inboundTranslationSliceData->Array.
|
|
947
|
-
let
|
|
958
|
+
let exposedInbound =
|
|
959
|
+
inboundTranslationSliceData->Array.filter(((_, _, _, _, exposed)) => exposed)
|
|
960
|
+
let inboundFieldNames = exposedInbound->Array.map(((_, fieldName, _, _, _)) => fieldName)
|
|
961
|
+
let inboundSchemas = exposedInbound->Array.map(((_, _, _, schema, _)) => schema)
|
|
948
962
|
|
|
949
963
|
// Collect DCB mutation field names + TAGs for AppSync resolver creation
|
|
950
964
|
// (excludes @noApi slices — includes both sync and async)
|
|
@@ -1092,7 +1106,13 @@ module Make = (
|
|
|
1092
1106
|
})
|
|
1093
1107
|
|
|
1094
1108
|
let mutationEntriesFromInboundSlices =
|
|
1095
|
-
inboundTranslationSlices
|
|
1109
|
+
inboundTranslationSlices
|
|
1110
|
+
->Array.filter((module(ITS: InboundTranslationSlice.T)) =>
|
|
1111
|
+
InboundTranslationSlice_Callback.exposesMutation(
|
|
1112
|
+
ITS.Spec.externalInputSchema->S.castToUnknown,
|
|
1113
|
+
)
|
|
1114
|
+
)
|
|
1115
|
+
->Array.map((module(ITS: InboundTranslationSlice.T)) => {
|
|
1096
1116
|
let fieldName = Api_Naming.sliceMutationField(~plugin=name, ~slice=ITS.Spec.name)
|
|
1097
1117
|
let fieldPermissions = Dict.make()
|
|
1098
1118
|
inboundDoorPermission(
|
|
@@ -1138,6 +1158,9 @@ module Make = (
|
|
|
1138
1158
|
}
|
|
1139
1159
|
})
|
|
1140
1160
|
|
|
1161
|
+
// What the framework keeps for a slice is read by the deployment's operators.
|
|
1162
|
+
let operators = Reventless.OwnerScope.operators()
|
|
1163
|
+
|
|
1141
1164
|
let automationEntries = automationSlices->Array.map((module(A: AutomationSlice.T)) => {
|
|
1142
1165
|
let qn = Api_Naming.queryFieldNamesForSliceQueryDb(~plugin=name, ~queryDbName=A.queryDbName)
|
|
1143
1166
|
{
|
|
@@ -1148,6 +1171,7 @@ module Make = (
|
|
|
1148
1171
|
A.Spec.todoItemSchema,
|
|
1149
1172
|
)->S.castToUnknown,
|
|
1150
1173
|
authorization: None,
|
|
1174
|
+
permission: operators,
|
|
1151
1175
|
connectionSpec: true,
|
|
1152
1176
|
}
|
|
1153
1177
|
})
|
|
@@ -1162,6 +1186,7 @@ module Make = (
|
|
|
1162
1186
|
returnTypeName: qn.returnTypeName,
|
|
1163
1187
|
stateSchema: AutomationSlice_Schedules.rowSchema->S.castToUnknown,
|
|
1164
1188
|
authorization: None,
|
|
1189
|
+
permission: operators,
|
|
1165
1190
|
connectionSpec: true,
|
|
1166
1191
|
}
|
|
1167
1192
|
})
|
|
@@ -1181,6 +1206,7 @@ module Make = (
|
|
|
1181
1206
|
O.Spec.outboundItemSchema,
|
|
1182
1207
|
)->S.castToUnknown,
|
|
1183
1208
|
authorization: None,
|
|
1209
|
+
permission: operators,
|
|
1184
1210
|
connectionSpec: true,
|
|
1185
1211
|
}
|
|
1186
1212
|
})
|
|
@@ -1197,6 +1223,7 @@ module Make = (
|
|
|
1197
1223
|
returnTypeName: qn.returnTypeName,
|
|
1198
1224
|
stateSchema: InboundTranslationSlice_Callback.auditRowSchema->S.castToUnknown,
|
|
1199
1225
|
authorization: None,
|
|
1226
|
+
permission: operators,
|
|
1200
1227
|
connectionSpec: true,
|
|
1201
1228
|
}
|
|
1202
1229
|
})
|
|
@@ -13,6 +13,7 @@ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js
|
|
|
13
13
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
14
14
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
15
15
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
16
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
16
17
|
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
17
18
|
import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
18
19
|
import * as DcbValidation$Reventless from "@reventlessdev/reventless-spec/src/components/DcbValidation.res.mjs";
|
|
@@ -406,19 +407,22 @@ function Make(DcbEventLogStorage) {
|
|
|
406
407
|
let inboundTranslationSliceData = inboundTranslationSlices.map(ITS => {
|
|
407
408
|
let its = ITS.make(publishJsons, componentRuntime[ITS.Spec.name], opts);
|
|
408
409
|
let fieldName = Api_Naming$ReventlessCore.sliceMutationField(name, ITS.Spec.name);
|
|
410
|
+
let exposed = InboundTranslationSlice_Callback$ReventlessCore.exposesMutation(ITS.Spec.externalInputSchema);
|
|
411
|
+
InboundTranslationSlice_Callback$ReventlessCore.checkReachable(name, ITS.Spec.name, exposed, Stdlib_Option.isSome(ITS.Spec.endpoint));
|
|
409
412
|
let registerResolver = HooksConfig.hooks.inboundMutationResolverHook;
|
|
410
|
-
if (registerResolver !== undefined) {
|
|
413
|
+
if (registerResolver !== undefined && exposed) {
|
|
411
414
|
registerResolver(fieldName, ITS.Spec.externalInputSchema, inboundDoorPermission(ITS.Spec.commandSchema, ITS.Spec.authorizationOf));
|
|
412
415
|
}
|
|
413
416
|
let bindReceive = HooksConfig.hooks.inboundMutationBindReceiveHook;
|
|
414
|
-
if (bindReceive !== undefined) {
|
|
415
|
-
Component$ReventlessCore.operations(its).apply(ops => bindReceive(fieldName, ops.receive));
|
|
417
|
+
if (bindReceive !== undefined && exposed) {
|
|
418
|
+
Component$ReventlessCore.operations(its).apply(ops => bindReceive(fieldName, (input, caller) => ops.receive(input, caller, undefined)));
|
|
416
419
|
}
|
|
417
420
|
return [
|
|
418
421
|
ITS.Spec.name,
|
|
419
422
|
fieldName,
|
|
420
423
|
its,
|
|
421
|
-
ITS.Spec.externalInputSchema
|
|
424
|
+
ITS.Spec.externalInputSchema,
|
|
425
|
+
exposed
|
|
422
426
|
];
|
|
423
427
|
});
|
|
424
428
|
let inboundTranslationSlicesOutputs = Object.fromEntries(inboundTranslationSliceData.map(param => [
|
|
@@ -481,7 +485,7 @@ function Make(DcbEventLogStorage) {
|
|
|
481
485
|
let receiveFn = receivers[event.fieldName];
|
|
482
486
|
if (receiveFn !== undefined) {
|
|
483
487
|
return Effect.promise(async () => {
|
|
484
|
-
let result = await receiveFn(event.arguments, InboundTranslationSlice_Callback$ReventlessCore.doorCaller(event.identity));
|
|
488
|
+
let result = await receiveFn(event.arguments, InboundTranslationSlice_Callback$ReventlessCore.doorCaller(event.identity), undefined);
|
|
485
489
|
return CommandTopic$ReventlessCore.commandOutcomeToJson(InboundTranslationSlice_Callback$ReventlessCore.receiveResultToOutcome(result));
|
|
486
490
|
});
|
|
487
491
|
} else {
|
|
@@ -506,8 +510,9 @@ function Make(DcbEventLogStorage) {
|
|
|
506
510
|
});
|
|
507
511
|
let dcbResources = Object.values(stateChangeSlicesOutputs).flatMap(outputs => outputs.resources).concat(Object.values(inboundTranslationSlicesOutputs).flatMap(outputs => outputs.resources.concat(outputs.queryDb.resources)));
|
|
508
512
|
let asyncDcbResources = Object.values(asyncStateChangeSlicesOutputs).flatMap(outputs => outputs.resources);
|
|
509
|
-
let
|
|
510
|
-
let
|
|
513
|
+
let exposedInbound = inboundTranslationSliceData.filter(param => param[4]);
|
|
514
|
+
let inboundFieldNames = exposedInbound.map(param => param[1]);
|
|
515
|
+
let inboundSchemas = exposedInbound.map(param => param[3]);
|
|
511
516
|
let dcbMutationData = stateChangeSlices.flatMap(S => {
|
|
512
517
|
let commandSchema = S.Spec.commandSchema;
|
|
513
518
|
if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
|
|
@@ -577,7 +582,7 @@ function Make(DcbEventLogStorage) {
|
|
|
577
582
|
consistencyRead: Stdlib_Option.flatMap(sliceDef, d => d.consistencyRead)
|
|
578
583
|
};
|
|
579
584
|
});
|
|
580
|
-
let mutationEntriesFromInboundSlices = inboundTranslationSlices.map(ITS => {
|
|
585
|
+
let mutationEntriesFromInboundSlices = inboundTranslationSlices.filter(ITS => InboundTranslationSlice_Callback$ReventlessCore.exposesMutation(ITS.Spec.externalInputSchema)).map(ITS => {
|
|
581
586
|
let fieldName = Api_Naming$ReventlessCore.sliceMutationField(name, ITS.Spec.name);
|
|
582
587
|
let fieldPermissions = {};
|
|
583
588
|
Stdlib_Option.forEach(inboundDoorPermission(ITS.Spec.commandSchema, ITS.Spec.authorizationOf), rule => {
|
|
@@ -612,6 +617,7 @@ function Make(DcbEventLogStorage) {
|
|
|
612
617
|
resolvedFields: resolvedFields
|
|
613
618
|
};
|
|
614
619
|
});
|
|
620
|
+
let operators = OwnerScope$Reventless.operators();
|
|
615
621
|
let automationEntries = automationSlices.map(A => {
|
|
616
622
|
let qn = Api_Naming$ReventlessCore.queryFieldNamesForSliceQueryDb(name, A.queryDbName, undefined);
|
|
617
623
|
return {
|
|
@@ -620,6 +626,7 @@ function Make(DcbEventLogStorage) {
|
|
|
620
626
|
returnTypeName: qn.returnTypeName,
|
|
621
627
|
stateSchema: AutomationSlice_Callback$ReventlessCore.todoRowSchemaFor(A.Spec.todoItemSchema),
|
|
622
628
|
authorization: undefined,
|
|
629
|
+
permission: operators,
|
|
623
630
|
connectionSpec: true
|
|
624
631
|
};
|
|
625
632
|
});
|
|
@@ -631,6 +638,7 @@ function Make(DcbEventLogStorage) {
|
|
|
631
638
|
returnTypeName: qn.returnTypeName,
|
|
632
639
|
stateSchema: AutomationSlice_Schedules$ReventlessCore.rowSchema,
|
|
633
640
|
authorization: undefined,
|
|
641
|
+
permission: operators,
|
|
634
642
|
connectionSpec: true
|
|
635
643
|
};
|
|
636
644
|
}));
|
|
@@ -642,6 +650,7 @@ function Make(DcbEventLogStorage) {
|
|
|
642
650
|
returnTypeName: qn.returnTypeName,
|
|
643
651
|
stateSchema: OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchemaFor(O.Spec.outboundItemSchema),
|
|
644
652
|
authorization: undefined,
|
|
653
|
+
permission: operators,
|
|
645
654
|
connectionSpec: true
|
|
646
655
|
};
|
|
647
656
|
});
|
|
@@ -653,6 +662,7 @@ function Make(DcbEventLogStorage) {
|
|
|
653
662
|
returnTypeName: qn.returnTypeName,
|
|
654
663
|
stateSchema: InboundTranslationSlice_Callback$ReventlessCore.auditRowSchema,
|
|
655
664
|
authorization: undefined,
|
|
665
|
+
permission: operators,
|
|
656
666
|
connectionSpec: true
|
|
657
667
|
};
|
|
658
668
|
});
|
|
@@ -36,7 +36,7 @@ module Make = (
|
|
|
36
36
|
let config = Reventless.ReadModel.config()
|
|
37
37
|
let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>> = None
|
|
38
38
|
type role = Reventless.Role.name
|
|
39
|
-
let authorization: Reventless.Authorization.rule<role> =
|
|
39
|
+
let authorization: Reventless.Authorization.rule<role> = Reventless.OwnerScope.operators()
|
|
40
40
|
let visibility: Reventless.Visibility.t = Public
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -81,8 +81,13 @@ module Make = (
|
|
|
81
81
|
->Pulumi.Output.all2
|
|
82
82
|
->Pulumi.Output.apply(((queryDbOps, publishJsonsFn)) => {
|
|
83
83
|
let ops: InboundTranslationSlice.operations = {
|
|
84
|
-
receive: async (inputJson, ~caller=?) => {
|
|
85
|
-
let result = await Callback.receive(publishJsonsFn, inputJson, ~caller?)
|
|
84
|
+
receive: async (inputJson, ~caller=?, ~door=?) => {
|
|
85
|
+
let result = await Callback.receive(publishJsonsFn, inputJson, ~caller?, ~door?)
|
|
86
|
+
await syncToQueryDb(queryDbOps, result->InboundTranslationSlice_Callback.requestIdOf)
|
|
87
|
+
result
|
|
88
|
+
},
|
|
89
|
+
refuse: async (~body, ~errorCode, error) => {
|
|
90
|
+
let result = Callback.refuse(~body, ~errorCode, error)
|
|
86
91
|
await syncToQueryDb(queryDbOps, result->InboundTranslationSlice_Callback.requestIdOf)
|
|
87
92
|
result
|
|
88
93
|
},
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
4
4
|
import * as Pulumi from "@pulumi/pulumi";
|
|
5
5
|
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
6
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
6
7
|
import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
7
8
|
import * as ComponentType$ReventlessCore from "../../ComponentType.res.mjs";
|
|
8
9
|
import * as QueryDb_Builder$ReventlessCore from "../QueryDb/QueryDb_Builder.res.mjs";
|
|
@@ -16,6 +17,7 @@ function Make(QueryDbStorage) {
|
|
|
16
17
|
let queryDbName = Spec.name + "Audit";
|
|
17
18
|
let moduleUrl = import.meta.url;
|
|
18
19
|
let config = ReadModel$Reventless.config(undefined, undefined, undefined);
|
|
20
|
+
let authorization = OwnerScope$Reventless.operators();
|
|
19
21
|
let SpecificQueryDb = QueryDb_Builder$ReventlessCore.Make({
|
|
20
22
|
Id: Id$Reventless.$$String,
|
|
21
23
|
name: queryDbName,
|
|
@@ -23,7 +25,7 @@ function Make(QueryDbStorage) {
|
|
|
23
25
|
stateSchema: InboundTranslationSlice_Callback$ReventlessCore.auditRowSchema,
|
|
24
26
|
config: config,
|
|
25
27
|
subIdConfig: undefined,
|
|
26
|
-
authorization:
|
|
28
|
+
authorization: authorization,
|
|
27
29
|
visibility: "Public"
|
|
28
30
|
})(QueryDbStorage)(QueryDbResolvers);
|
|
29
31
|
let syncToQueryDb = async (queryDbOps, requestId) => {
|
|
@@ -55,8 +57,13 @@ function Make(QueryDbStorage) {
|
|
|
55
57
|
let publishJsonsFn = param[1];
|
|
56
58
|
let queryDbOps = param[0];
|
|
57
59
|
return {
|
|
58
|
-
receive: async (inputJson, caller) => {
|
|
59
|
-
let result = await Callback.receive(publishJsonsFn, inputJson, caller);
|
|
60
|
+
receive: async (inputJson, caller, door) => {
|
|
61
|
+
let result = await Callback.receive(publishJsonsFn, inputJson, caller, door);
|
|
62
|
+
await syncToQueryDb(queryDbOps, InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result));
|
|
63
|
+
return result;
|
|
64
|
+
},
|
|
65
|
+
refuse: async (body, errorCode, error) => {
|
|
66
|
+
let result = Callback.refuse(body, errorCode, error);
|
|
60
67
|
await syncToQueryDb(queryDbOps, InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result));
|
|
61
68
|
return result;
|
|
62
69
|
}
|
|
@@ -15,6 +15,8 @@ type auditRow = {
|
|
|
15
15
|
commandCount?: int,
|
|
16
16
|
error?: string,
|
|
17
17
|
receivedAt: string,
|
|
18
|
+
/** Absent on rows written before the slice had a second door. */
|
|
19
|
+
door?: ReventlessInfra.InboundTranslationSlice.door,
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** The `CommandResult` a `receive` outcome answers with: the first target as
|
|
@@ -76,6 +78,19 @@ type doorInvocation = {
|
|
|
76
78
|
let doorCaller = (identity: Nullable.t<Reventless.Identity.t>): Reventless.Identity.t =>
|
|
77
79
|
identity->Nullable.toOption->Option.getOr(Reventless.Identity.anonymous)
|
|
78
80
|
|
|
81
|
+
/** Whether a slice's input is offered as a mutation: not when `externalInput`
|
|
82
|
+
carries `@noApi`, which leaves the webhook as its only door. */
|
|
83
|
+
let exposesMutation = (externalInputSchema: S.t<unknown>): bool =>
|
|
84
|
+
!ApiNoApiHelpers.isNoApi(externalInputSchema)
|
|
85
|
+
|
|
86
|
+
/** Refuse a slice with neither door, which nothing could ever reach. */
|
|
87
|
+
let checkReachable = (~plugin: string, ~slice: string, ~mutation: bool, ~webhook: bool) =>
|
|
88
|
+
if !mutation && !webhook {
|
|
89
|
+
JsError.throwWithMessage(
|
|
90
|
+
`${plugin}: inbound translation slice ${slice} has @noApi on externalInput and declares no endpoint, so nothing can reach it`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
module type T = {
|
|
80
95
|
module Spec: Reventless.InboundTranslationSlice.Spec
|
|
81
96
|
module Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec
|
|
@@ -88,7 +103,18 @@ module type T = {
|
|
|
88
103
|
ReventlessInfra.CommandTopic.publishJsons,
|
|
89
104
|
JSON.t,
|
|
90
105
|
~caller: Reventless.Identity.t=?,
|
|
106
|
+
~door: ReventlessInfra.InboundTranslationSlice.door=?,
|
|
91
107
|
) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>
|
|
108
|
+
|
|
109
|
+
/** Record a webhook request refused before it could be read, and reject it. */
|
|
110
|
+
let refuse: (
|
|
111
|
+
~body: string,
|
|
112
|
+
~errorCode: string,
|
|
113
|
+
string,
|
|
114
|
+
) => ReventlessInfra.InboundTranslationSlice.receiveResult
|
|
115
|
+
|
|
116
|
+
/** The slice's webhook route, if its spec declares an endpoint. */
|
|
117
|
+
let webhook: option<InboundTranslationSlice_Webhook.route>
|
|
92
118
|
}
|
|
93
119
|
|
|
94
120
|
module Make = (
|
|
@@ -110,15 +136,12 @@ module Make = (
|
|
|
110
136
|
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr(fallback)
|
|
111
137
|
|
|
112
138
|
// One audit row per request, whatever the outcome.
|
|
113
|
-
let failWith = (~requestId, ~
|
|
114
|
-
auditLog->Dict.set(
|
|
115
|
-
requestId,
|
|
116
|
-
{input: inputJson->JSON.stringify, status: Failure, error, receivedAt: now()},
|
|
117
|
-
)
|
|
139
|
+
let failWith = (~requestId, ~input, ~door, ~errorCode=?, error) => {
|
|
140
|
+
auditLog->Dict.set(requestId, {input, status: Failure, error, receivedAt: now(), door})
|
|
118
141
|
Error({ReventlessInfra.InboundTranslationSlice.requestId, error, ?errorCode})
|
|
119
142
|
}
|
|
120
143
|
|
|
121
|
-
let succeed = (~requestId, ~inputJson, targetIds) => {
|
|
144
|
+
let succeed = (~requestId, ~inputJson, ~door, targetIds) => {
|
|
122
145
|
let commandCount = targetIds->Array.length
|
|
123
146
|
auditLog->Dict.set(
|
|
124
147
|
requestId,
|
|
@@ -128,6 +151,7 @@ module Make = (
|
|
|
128
151
|
targetIds,
|
|
129
152
|
commandCount,
|
|
130
153
|
receivedAt: now(),
|
|
154
|
+
door,
|
|
131
155
|
},
|
|
132
156
|
)
|
|
133
157
|
Ok({ReventlessInfra.InboundTranslationSlice.requestId, targetIds, commandCount})
|
|
@@ -155,9 +179,12 @@ module Make = (
|
|
|
155
179
|
publishJsons: ReventlessInfra.CommandTopic.publishJsons,
|
|
156
180
|
inputJson: JSON.t,
|
|
157
181
|
~caller: option<Reventless.Identity.t>=?,
|
|
182
|
+
~door: ReventlessInfra.InboundTranslationSlice.door=Mutation,
|
|
158
183
|
): ReventlessInfra.InboundTranslationSlice.receiveResult => {
|
|
159
184
|
let requestId = Uuid.v4()
|
|
160
|
-
let fail = (~errorCode=?, error) =>
|
|
185
|
+
let fail = (~errorCode=?, error) =>
|
|
186
|
+
failWith(~requestId, ~input=inputJson->JSON.stringify, ~door, ~errorCode?, error)
|
|
187
|
+
let succeed = succeed(~requestId, ~inputJson, ~door, ...)
|
|
161
188
|
let parsed = try Ok(inputJson->S.parseOrThrow(~to=Spec.externalInputSchema)) catch {
|
|
162
189
|
| exn => Error(messageOf(exn, "invalid input"))
|
|
163
190
|
}
|
|
@@ -192,16 +219,24 @@ module Make = (
|
|
|
192
219
|
~errorCode="Forbidden",
|
|
193
220
|
`${Spec.name}: the caller is not authorized for every command this input translates into`,
|
|
194
221
|
)
|
|
195
|
-
| Ok([]) => succeed(
|
|
222
|
+
| Ok([]) => succeed([])
|
|
196
223
|
| Ok(msgs) =>
|
|
197
224
|
try {
|
|
198
225
|
await publishJsons(msgs)
|
|
199
|
-
succeed(
|
|
226
|
+
succeed(pairs->Array.map(((targetId, _)) => targetId))
|
|
200
227
|
} catch {
|
|
201
|
-
| exn => fail(messageOf(exn, "publish failed"))
|
|
228
|
+
| exn => fail(~errorCode="PublishFailed", messageOf(exn, "publish failed"))
|
|
202
229
|
}
|
|
203
230
|
}
|
|
204
231
|
}
|
|
205
232
|
}
|
|
206
233
|
}
|
|
234
|
+
|
|
235
|
+
let refuse = (~body, ~errorCode, error) =>
|
|
236
|
+
failWith(~requestId=Uuid.v4(), ~input=body, ~door=Webhook, ~errorCode, error)
|
|
237
|
+
|
|
238
|
+
let webhook =
|
|
239
|
+
Spec.endpoint->Option.map(endpoint =>
|
|
240
|
+
InboundTranslationSlice_Webhook.routeOf(~slice=Spec.name, endpoint)
|
|
241
|
+
)
|
|
207
242
|
}
|