@reventlessdev/reventless-core 3.0.0-alpha.290 → 3.0.0-alpha.292
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 +18 -0
- package/package.json +7 -7
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +25 -46
- package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +27 -46
- package/src/components/AutomationSlice/AutomationSlice_Route.res +50 -0
- package/src/components/AutomationSlice/AutomationSlice_Route.res.mjs +39 -0
- package/src/components/Dcb/Dcb_Builder.res +27 -43
- package/src/components/Dcb/Dcb_Builder.res.mjs +10 -21
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +2 -2
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res.mjs +2 -2
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res +118 -160
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +121 -130
- package/src/plugin/component/Plugin_Helpers.res +11 -3
- package/src/plugin/component/Plugin_Structure.res +22 -9
- package/src/plugin/component/Plugin_Structure.res.mjs +11 -1
- package/tests/plugin/PluginStructureAccessTest.res +56 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +87 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// InboundTranslationSlice callback
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// InboundTranslationSlice callback: parses external input, translates it into
|
|
2
|
+
// commands, checks each against its own rule, and publishes them. Runtime-pure,
|
|
3
|
+
// since the deployed DCB command Lambda imports it.
|
|
4
4
|
|
|
5
5
|
@schema
|
|
6
6
|
type auditStatus =
|
|
@@ -17,21 +17,8 @@ type auditRow = {
|
|
|
17
17
|
receivedAt: string,
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
declares, so both surfaces feed `CommandTopic.commandOutcomeToJson` rather than
|
|
23
|
-
hand-rolling a second `__typename` writer.
|
|
24
|
-
|
|
25
|
-
A translation can fan out across several targets; the mutation response reports
|
|
26
|
-
the first target as `entityId` and the fan-out count as `eventCount`. The
|
|
27
|
-
per-target detail stays queryable through the slice's audit read model. When the
|
|
28
|
-
translation legitimately produced no command, `entityId` is omitted (encoding as
|
|
29
|
-
`null`) rather than reporting a target that does not exist.
|
|
30
|
-
|
|
31
|
-
Lives here rather than in `InboundTranslationSlice.res` because the deployed DCB
|
|
32
|
-
command Lambda calls it at runtime, and the base module imports `@pulumi/pulumi`
|
|
33
|
-
at deploy time — keeping this runtime-pure keeps Pulumi out of the Lambda graph.
|
|
34
|
-
*/
|
|
20
|
+
/** The `CommandResult` a `receive` outcome answers with: the first target as
|
|
21
|
+
`entityId` (omitted when nothing was produced), the fan-out as `eventCount`. */
|
|
35
22
|
let receiveResultToOutcome = (
|
|
36
23
|
result: ReventlessInfra.InboundTranslationSlice.receiveResult,
|
|
37
24
|
): CommandTopic.commandOutcome =>
|
|
@@ -41,37 +28,54 @@ let receiveResultToOutcome = (
|
|
|
41
28
|
| Some(entityId) => Accepted({msgId: requestId, entityId, eventCount: commandCount})
|
|
42
29
|
| None => Accepted({msgId: requestId, eventCount: commandCount})
|
|
43
30
|
}
|
|
44
|
-
| Error({requestId, error}) =>
|
|
45
|
-
Rejected({
|
|
31
|
+
| Error({requestId, error, ?errorCode}) =>
|
|
32
|
+
Rejected({
|
|
33
|
+
msgId: requestId,
|
|
34
|
+
errorCode: errorCode->Option.getOr("TranslationFailed"),
|
|
35
|
+
errorDetail: Some(error),
|
|
36
|
+
})
|
|
46
37
|
}
|
|
47
38
|
|
|
48
|
-
/**
|
|
49
|
-
The request a `receive` outcome belongs to, from either arm. Both carry the id
|
|
50
|
-
that keys the outcome's audit row, so a caller draining the log does not have to
|
|
51
|
-
match on the result to find its own row.
|
|
52
|
-
*/
|
|
39
|
+
/** The request a `receive` outcome belongs to, from either arm. */
|
|
53
40
|
let requestIdOf = (result: ReventlessInfra.InboundTranslationSlice.receiveResult): string =>
|
|
54
41
|
switch result {
|
|
55
42
|
| Ok({requestId}) | Error({requestId}) => requestId
|
|
56
43
|
}
|
|
57
44
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`auditLog` is a hand-off buffer between `receive` and whoever persists the row,
|
|
62
|
-
not a log in its own right — the QueryDb it drains into is the durable one. It
|
|
63
|
-
has to be emptied as it drains: the dict lives as long as the process (a warm
|
|
64
|
-
Lambda container serves many requests), so a drain that walked the whole dict
|
|
65
|
-
would rewrite every row the container had ever seen on every request — quadratic
|
|
66
|
-
writes, unbounded retention, and, because the write is an overwrite by row id,
|
|
67
|
-
resurrection of rows deleted from the table since.
|
|
68
|
-
*/
|
|
45
|
+
/** Remove one request's audit row from the in-memory log and hand it back. The
|
|
46
|
+
log outlives a request in a warm process, so draining all of it would rewrite
|
|
47
|
+
every row it ever held. */
|
|
69
48
|
let takeAuditRow = (auditLog: Dict.t<auditRow>, requestId: string): option<auditRow> => {
|
|
70
49
|
let row = auditLog->Dict.get(requestId)
|
|
71
50
|
auditLog->Dict.delete(requestId)
|
|
72
51
|
row
|
|
73
52
|
}
|
|
74
53
|
|
|
54
|
+
/** Whether `caller` may send a command under `rule`. No caller, or a system
|
|
55
|
+
(IAM) caller, is the platform acting for itself and passes every rule. */
|
|
56
|
+
let callerAdmits = (rule: Reventless.Authorization.permission, caller) =>
|
|
57
|
+
switch caller {
|
|
58
|
+
| None => true
|
|
59
|
+
| Some(identity) =>
|
|
60
|
+
switch Reventless.OwnerScope.classify(identity, ~elevated=[]) {
|
|
61
|
+
| System => true
|
|
62
|
+
| _ => Reventless.Authorization.isAllowed(rule, identity)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What an inbound mutation's resolver sends a deployed handler. `identity` is
|
|
67
|
+
built by the resolver, and is null where the transport identified nobody. */
|
|
68
|
+
type doorInvocation = {
|
|
69
|
+
fieldName: string,
|
|
70
|
+
arguments: JSON.t,
|
|
71
|
+
identity: Nullable.t<Reventless.Identity.t>,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The caller a door hands on: what its transport identified, else anonymous, so
|
|
75
|
+
a door that lost the identity refuses rather than acts as the platform. */
|
|
76
|
+
let doorCaller = (identity: Nullable.t<Reventless.Identity.t>): Reventless.Identity.t =>
|
|
77
|
+
identity->Nullable.toOption->Option.getOr(Reventless.Identity.anonymous)
|
|
78
|
+
|
|
75
79
|
module type T = {
|
|
76
80
|
module Spec: Reventless.InboundTranslationSlice.Spec
|
|
77
81
|
module Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec
|
|
@@ -83,6 +87,7 @@ module type T = {
|
|
|
83
87
|
let receive: (
|
|
84
88
|
ReventlessInfra.CommandTopic.publishJsons,
|
|
85
89
|
JSON.t,
|
|
90
|
+
~caller: Reventless.Identity.t=?,
|
|
86
91
|
) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -97,149 +102,102 @@ module Make = (
|
|
|
97
102
|
|
|
98
103
|
let now = () => Date.make()->Date.toISOString
|
|
99
104
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// of a fresh correlation chain. `traceparent` populated from an inbound HTTP
|
|
103
|
-
// header would need to be threaded in here by the API/ingress adapter.
|
|
104
|
-
// `service` names the command's TARGET, as on the API path — see the note in
|
|
105
|
-
// `AutomationSlice_Callback`. The target's events are dispatched on it.
|
|
105
|
+
// External input has no upstream meta, so each command starts a correlation
|
|
106
|
+
// chain. `service` names the command's target, on which its events dispatch.
|
|
106
107
|
let makeMeta = (): Reventless.Message.meta => Message.generateMeta(~service=Spec.targetName)
|
|
107
108
|
|
|
109
|
+
let messageOf = (exn, fallback) =>
|
|
110
|
+
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr(fallback)
|
|
111
|
+
|
|
112
|
+
// One audit row per request, whatever the outcome.
|
|
113
|
+
let failWith = (~requestId, ~inputJson, ~errorCode=?, error) => {
|
|
114
|
+
auditLog->Dict.set(
|
|
115
|
+
requestId,
|
|
116
|
+
{input: inputJson->JSON.stringify, status: Failure, error, receivedAt: now()},
|
|
117
|
+
)
|
|
118
|
+
Error({ReventlessInfra.InboundTranslationSlice.requestId, error, ?errorCode})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let succeed = (~requestId, ~inputJson, targetIds) => {
|
|
122
|
+
let commandCount = targetIds->Array.length
|
|
123
|
+
auditLog->Dict.set(
|
|
124
|
+
requestId,
|
|
125
|
+
{
|
|
126
|
+
input: inputJson->JSON.stringify,
|
|
127
|
+
status: Success,
|
|
128
|
+
targetIds,
|
|
129
|
+
commandCount,
|
|
130
|
+
receivedAt: now(),
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
Ok({ReventlessInfra.InboundTranslationSlice.requestId, targetIds, commandCount})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Encoded with the command's schema, the one its target decodes with.
|
|
137
|
+
let encode = ((targetId, cmd): (string, Spec.command)): result<
|
|
138
|
+
Reventless.Message.commandJson,
|
|
139
|
+
string,
|
|
140
|
+
> =>
|
|
141
|
+
try Ok({
|
|
142
|
+
id: targetId,
|
|
143
|
+
meta: makeMeta(),
|
|
144
|
+
commandJson: cmd->Reventless.Util_Sury.toJson(Spec.commandSchema),
|
|
145
|
+
}) catch {
|
|
146
|
+
| exn =>
|
|
147
|
+
EffectLogger.logError(
|
|
148
|
+
~comp=`InboundTranslationSlice(${Spec.name})`,
|
|
149
|
+
`failed to encode command: ${messageOf(exn, "unknown")}`,
|
|
150
|
+
)->Effect.runSync
|
|
151
|
+
Error("failed to encode command")
|
|
152
|
+
}
|
|
153
|
+
|
|
108
154
|
let receive = async (
|
|
109
155
|
publishJsons: ReventlessInfra.CommandTopic.publishJsons,
|
|
110
156
|
inputJson: JSON.t,
|
|
157
|
+
~caller: option<Reventless.Identity.t>=?,
|
|
111
158
|
): ReventlessInfra.InboundTranslationSlice.receiveResult => {
|
|
112
159
|
let requestId = Uuid.v4()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
| exn =>
|
|
117
|
-
let msg =
|
|
118
|
-
exn
|
|
119
|
-
->JsExn.fromException
|
|
120
|
-
->Option.flatMap(JsExn.message)
|
|
121
|
-
->Option.getOr("invalid input")
|
|
122
|
-
Error(msg)
|
|
160
|
+
let fail = (~errorCode=?, error) => failWith(~requestId, ~inputJson, ~errorCode?, error)
|
|
161
|
+
let parsed = try Ok(inputJson->S.parseOrThrow(~to=Spec.externalInputSchema)) catch {
|
|
162
|
+
| exn => Error(messageOf(exn, "invalid input"))
|
|
123
163
|
}
|
|
124
164
|
|
|
125
|
-
switch
|
|
126
|
-
| Error(msg) =>
|
|
127
|
-
auditLog->Dict.set(
|
|
128
|
-
requestId,
|
|
129
|
-
{
|
|
130
|
-
input: inputJson->JSON.stringify,
|
|
131
|
-
status: Failure,
|
|
132
|
-
error: msg,
|
|
133
|
-
receivedAt: now(),
|
|
134
|
-
},
|
|
135
|
-
)
|
|
136
|
-
Error({requestId, error: msg})
|
|
137
|
-
|
|
165
|
+
switch parsed {
|
|
166
|
+
| Error(msg) => fail(msg)
|
|
138
167
|
| Ok(input) =>
|
|
139
168
|
switch Translation.translate(input) {
|
|
169
|
+
| Error(msg) => fail(msg)
|
|
140
170
|
| Ok(pairs) =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
input: inputJson->JSON.stringify,
|
|
146
|
-
status: Success,
|
|
147
|
-
targetIds: [],
|
|
148
|
-
commandCount: 0,
|
|
149
|
-
receivedAt: now(),
|
|
150
|
-
},
|
|
171
|
+
// Each command against its own rule; one refused refuses the message.
|
|
172
|
+
let refused =
|
|
173
|
+
pairs->Array.find(((_, cmd)) =>
|
|
174
|
+
!callerAdmits(Spec.commandAuthorization(cmd)->Reventless.Authorization.named, caller)
|
|
151
175
|
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
meta: makeMeta(),
|
|
165
|
-
commandJson,
|
|
166
|
-
}
|
|
167
|
-
msgs.contents = msgs.contents->Array.concat([msg])
|
|
168
|
-
} catch {
|
|
169
|
-
| exn =>
|
|
170
|
-
let errMsg =
|
|
171
|
-
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown")
|
|
172
|
-
EffectLogger.logError(
|
|
173
|
-
~comp=`InboundTranslationSlice(${Spec.name})`,
|
|
174
|
-
`failed to encode command: ${errMsg}`,
|
|
175
|
-
)->Effect.runSync
|
|
176
|
-
encodeError := Some("failed to encode command")
|
|
177
|
-
}
|
|
176
|
+
switch refused {
|
|
177
|
+
| Some(_) =>
|
|
178
|
+
fail(
|
|
179
|
+
~errorCode="Forbidden",
|
|
180
|
+
`${Spec.name}: the caller is not authorized for every command this input translates into`,
|
|
181
|
+
)
|
|
182
|
+
| None =>
|
|
183
|
+
switch pairs->Array.reduce(Ok([]), (acc, pair) =>
|
|
184
|
+
switch (acc, encode(pair)) {
|
|
185
|
+
| (Ok(msgs), Ok(msg)) => Ok(msgs->Array.concat([msg]))
|
|
186
|
+
| (Error(_) as failed, _) => failed
|
|
187
|
+
| (_, Error(msg)) => Error(msg)
|
|
178
188
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
|
183
|
-
auditLog->Dict.set(
|
|
184
|
-
requestId,
|
|
185
|
-
{
|
|
186
|
-
input: inputJson->JSON.stringify,
|
|
187
|
-
status: Failure,
|
|
188
|
-
error: msg,
|
|
189
|
-
receivedAt: now(),
|
|
190
|
-
},
|
|
191
|
-
)
|
|
192
|
-
Error({requestId, error: msg})
|
|
193
|
-
| None =>
|
|
189
|
+
) {
|
|
190
|
+
| Error(msg) => fail(msg)
|
|
191
|
+
| Ok([]) => succeed(~requestId, ~inputJson, [])
|
|
192
|
+
| Ok(msgs) =>
|
|
194
193
|
try {
|
|
195
|
-
await publishJsons(msgs
|
|
196
|
-
|
|
197
|
-
let (targetId, _) = pair
|
|
198
|
-
targetId
|
|
199
|
-
})
|
|
200
|
-
auditLog->Dict.set(
|
|
201
|
-
requestId,
|
|
202
|
-
{
|
|
203
|
-
input: inputJson->JSON.stringify,
|
|
204
|
-
status: Success,
|
|
205
|
-
targetIds,
|
|
206
|
-
commandCount: pairs->Array.length,
|
|
207
|
-
receivedAt: now(),
|
|
208
|
-
},
|
|
209
|
-
)
|
|
210
|
-
Ok({requestId, targetIds, commandCount: pairs->Array.length})
|
|
194
|
+
await publishJsons(msgs)
|
|
195
|
+
succeed(~requestId, ~inputJson, pairs->Array.map(((targetId, _)) => targetId))
|
|
211
196
|
} catch {
|
|
212
|
-
| exn =>
|
|
213
|
-
let msg =
|
|
214
|
-
exn
|
|
215
|
-
->JsExn.fromException
|
|
216
|
-
->Option.flatMap(JsExn.message)
|
|
217
|
-
->Option.getOr("publish failed")
|
|
218
|
-
auditLog->Dict.set(
|
|
219
|
-
requestId,
|
|
220
|
-
{
|
|
221
|
-
input: inputJson->JSON.stringify,
|
|
222
|
-
status: Failure,
|
|
223
|
-
error: msg,
|
|
224
|
-
receivedAt: now(),
|
|
225
|
-
},
|
|
226
|
-
)
|
|
227
|
-
Error({requestId, error: msg})
|
|
197
|
+
| exn => fail(messageOf(exn, "publish failed"))
|
|
228
198
|
}
|
|
229
199
|
}
|
|
230
200
|
}
|
|
231
|
-
|
|
232
|
-
| Error(msg) =>
|
|
233
|
-
auditLog->Dict.set(
|
|
234
|
-
requestId,
|
|
235
|
-
{
|
|
236
|
-
input: inputJson->JSON.stringify,
|
|
237
|
-
status: Failure,
|
|
238
|
-
error: msg,
|
|
239
|
-
receivedAt: now(),
|
|
240
|
-
},
|
|
241
|
-
)
|
|
242
|
-
Error({requestId, error: msg})
|
|
243
201
|
}
|
|
244
202
|
}
|
|
245
203
|
}
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
4
|
import * as Uuid from "uuid";
|
|
5
5
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
6
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
7
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
7
8
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
9
|
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
11
|
+
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
9
12
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
13
|
+
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
14
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
10
15
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
16
|
+
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
11
17
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
12
18
|
|
|
13
19
|
let auditStatusSchema = Sury.union([
|
|
@@ -49,7 +55,7 @@ function receiveResultToOutcome(result) {
|
|
|
49
55
|
return {
|
|
50
56
|
TAG: "Rejected",
|
|
51
57
|
msgId: match$1.requestId,
|
|
52
|
-
errorCode: "TranslationFailed",
|
|
58
|
+
errorCode: Stdlib_Option.getOr(match$1.errorCode, "TranslationFailed"),
|
|
53
59
|
errorDetail: match$1.error
|
|
54
60
|
};
|
|
55
61
|
}
|
|
@@ -64,158 +70,141 @@ function takeAuditRow(auditLog, requestId) {
|
|
|
64
70
|
return row;
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
function callerAdmits(rule, caller) {
|
|
74
|
+
if (caller === undefined) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
let match = OwnerScope$Reventless.classify(caller, []);
|
|
78
|
+
if (typeof match !== "object") {
|
|
79
|
+
return true;
|
|
80
|
+
} else {
|
|
81
|
+
return Authorization$Reventless.isAllowed(rule, caller);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function doorCaller(identity) {
|
|
86
|
+
return Stdlib_Option.getOr((identity == null) ? undefined : Primitive_option.some(identity), Identity$Reventless.anonymous);
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
function Make(Spec) {
|
|
68
90
|
return Translation => {
|
|
69
91
|
let auditLog = {};
|
|
70
92
|
let makeMeta = () => Message$ReventlessCore.generateMeta(Spec.targetName, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
let
|
|
93
|
+
let messageOf = (exn, fallback) => Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), fallback);
|
|
94
|
+
let succeed = (requestId, inputJson, targetIds) => {
|
|
95
|
+
let commandCount = targetIds.length;
|
|
96
|
+
auditLog[requestId] = {
|
|
97
|
+
input: JSON.stringify(inputJson),
|
|
98
|
+
status: "Success",
|
|
99
|
+
targetIds: targetIds,
|
|
100
|
+
commandCount: commandCount,
|
|
101
|
+
receivedAt: new Date().toISOString()
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
TAG: "Ok",
|
|
105
|
+
_0: {
|
|
106
|
+
requestId: requestId,
|
|
107
|
+
targetIds: targetIds,
|
|
108
|
+
commandCount: commandCount
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
let encode = param => {
|
|
74
113
|
try {
|
|
75
|
-
|
|
114
|
+
return {
|
|
76
115
|
TAG: "Ok",
|
|
77
|
-
_0:
|
|
116
|
+
_0: {
|
|
117
|
+
id: param[0],
|
|
118
|
+
meta: makeMeta(),
|
|
119
|
+
commandJson: Util_Sury$Reventless.toJson(param[1], Spec.commandSchema)
|
|
120
|
+
}
|
|
78
121
|
};
|
|
79
122
|
} catch (raw_exn) {
|
|
80
123
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
81
|
-
|
|
82
|
-
|
|
124
|
+
Effect.runSync(EffectLogger$ReventlessCore.logError(`InboundTranslationSlice(` + Spec.name + `)`, undefined, `failed to encode command: ` + messageOf(exn, "unknown")));
|
|
125
|
+
return {
|
|
83
126
|
TAG: "Error",
|
|
84
|
-
_0:
|
|
127
|
+
_0: "failed to encode command"
|
|
85
128
|
};
|
|
86
129
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (pairs$1.length === 0) {
|
|
92
|
-
auditLog[requestId] = {
|
|
93
|
-
input: JSON.stringify(inputJson),
|
|
94
|
-
status: "Success",
|
|
95
|
-
targetIds: [],
|
|
96
|
-
commandCount: 0,
|
|
97
|
-
receivedAt: new Date().toISOString()
|
|
98
|
-
};
|
|
99
|
-
return {
|
|
100
|
-
TAG: "Ok",
|
|
101
|
-
_0: {
|
|
102
|
-
requestId: requestId,
|
|
103
|
-
targetIds: [],
|
|
104
|
-
commandCount: 0
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
let msgs = {
|
|
109
|
-
contents: []
|
|
110
|
-
};
|
|
111
|
-
let encodeError = {
|
|
112
|
-
contents: undefined
|
|
113
|
-
};
|
|
114
|
-
pairs$1.forEach(pair => {
|
|
115
|
-
if (!Stdlib_Option.isNone(encodeError.contents)) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
try {
|
|
119
|
-
let commandJson = JSON.parse(Stdlib_Option.getOrThrow(JSON.stringify(pair[1]), undefined));
|
|
120
|
-
let msg_id = pair[0];
|
|
121
|
-
let msg_meta = makeMeta();
|
|
122
|
-
let msg = {
|
|
123
|
-
id: msg_id,
|
|
124
|
-
meta: msg_meta,
|
|
125
|
-
commandJson: commandJson
|
|
126
|
-
};
|
|
127
|
-
msgs.contents = msgs.contents.concat([msg]);
|
|
128
|
-
return;
|
|
129
|
-
} catch (raw_exn) {
|
|
130
|
-
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
131
|
-
let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
|
|
132
|
-
Effect.runSync(EffectLogger$ReventlessCore.logError(`InboundTranslationSlice(` + Spec.name + `)`, undefined, `failed to encode command: ` + errMsg));
|
|
133
|
-
encodeError.contents = "failed to encode command";
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
let msg$1 = encodeError.contents;
|
|
138
|
-
if (msg$1 !== undefined) {
|
|
139
|
-
auditLog[requestId] = {
|
|
140
|
-
input: JSON.stringify(inputJson),
|
|
141
|
-
status: "Failure",
|
|
142
|
-
error: msg$1,
|
|
143
|
-
receivedAt: new Date().toISOString()
|
|
144
|
-
};
|
|
145
|
-
return {
|
|
146
|
-
TAG: "Error",
|
|
147
|
-
_0: {
|
|
148
|
-
requestId: requestId,
|
|
149
|
-
error: msg$1
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
try {
|
|
154
|
-
await publishJsons(msgs.contents);
|
|
155
|
-
let targetIds = pairs$1.map(pair => pair[0]);
|
|
156
|
-
auditLog[requestId] = {
|
|
157
|
-
input: JSON.stringify(inputJson),
|
|
158
|
-
status: "Success",
|
|
159
|
-
targetIds: targetIds,
|
|
160
|
-
commandCount: pairs$1.length,
|
|
161
|
-
receivedAt: new Date().toISOString()
|
|
162
|
-
};
|
|
163
|
-
return {
|
|
164
|
-
TAG: "Ok",
|
|
165
|
-
_0: {
|
|
166
|
-
requestId: requestId,
|
|
167
|
-
targetIds: targetIds,
|
|
168
|
-
commandCount: pairs$1.length
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
} catch (raw_exn$1) {
|
|
172
|
-
let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
|
|
173
|
-
let msg$2 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn$1), Stdlib_JsExn.message), "publish failed");
|
|
174
|
-
auditLog[requestId] = {
|
|
175
|
-
input: JSON.stringify(inputJson),
|
|
176
|
-
status: "Failure",
|
|
177
|
-
error: msg$2,
|
|
178
|
-
receivedAt: new Date().toISOString()
|
|
179
|
-
};
|
|
180
|
-
return {
|
|
181
|
-
TAG: "Error",
|
|
182
|
-
_0: {
|
|
183
|
-
requestId: requestId,
|
|
184
|
-
error: msg$2
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
let msg$3 = pairs._0;
|
|
190
|
-
auditLog[requestId] = {
|
|
191
|
-
input: JSON.stringify(inputJson),
|
|
192
|
-
status: "Failure",
|
|
193
|
-
error: msg$3,
|
|
194
|
-
receivedAt: new Date().toISOString()
|
|
195
|
-
};
|
|
196
|
-
return {
|
|
197
|
-
TAG: "Error",
|
|
198
|
-
_0: {
|
|
199
|
-
requestId: requestId,
|
|
200
|
-
error: msg$3
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
} else {
|
|
205
|
-
let msg$4 = input._0;
|
|
130
|
+
};
|
|
131
|
+
let receive = async (publishJsons, inputJson, caller) => {
|
|
132
|
+
let requestId = Uuid.v4();
|
|
133
|
+
let fail = (errorCode, error) => {
|
|
206
134
|
auditLog[requestId] = {
|
|
207
135
|
input: JSON.stringify(inputJson),
|
|
208
136
|
status: "Failure",
|
|
209
|
-
error:
|
|
137
|
+
error: error,
|
|
210
138
|
receivedAt: new Date().toISOString()
|
|
211
139
|
};
|
|
212
140
|
return {
|
|
213
141
|
TAG: "Error",
|
|
214
142
|
_0: {
|
|
215
143
|
requestId: requestId,
|
|
216
|
-
error:
|
|
144
|
+
error: error,
|
|
145
|
+
errorCode: errorCode
|
|
217
146
|
}
|
|
218
147
|
};
|
|
148
|
+
};
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = {
|
|
152
|
+
TAG: "Ok",
|
|
153
|
+
_0: Sury.parseOrThrow(inputJson, Spec.externalInputSchema)
|
|
154
|
+
};
|
|
155
|
+
} catch (raw_exn) {
|
|
156
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
157
|
+
parsed = {
|
|
158
|
+
TAG: "Error",
|
|
159
|
+
_0: messageOf(exn, "invalid input")
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (parsed.TAG !== "Ok") {
|
|
163
|
+
return fail(undefined, parsed._0);
|
|
164
|
+
}
|
|
165
|
+
let msg = Translation.translate(parsed._0);
|
|
166
|
+
if (msg.TAG !== "Ok") {
|
|
167
|
+
return fail(undefined, msg._0);
|
|
168
|
+
}
|
|
169
|
+
let pairs = msg._0;
|
|
170
|
+
let refused = pairs.find(param => !callerAdmits(Authorization$Reventless.named(Spec.commandAuthorization(param[1])), caller));
|
|
171
|
+
if (refused !== undefined) {
|
|
172
|
+
return fail("Forbidden", Spec.name + `: the caller is not authorized for every command this input translates into`);
|
|
173
|
+
}
|
|
174
|
+
let msg$1 = Stdlib_Array.reduce(pairs, {
|
|
175
|
+
TAG: "Ok",
|
|
176
|
+
_0: []
|
|
177
|
+
}, (acc, pair) => {
|
|
178
|
+
let match = encode(pair);
|
|
179
|
+
if (acc.TAG === "Ok") {
|
|
180
|
+
if (match.TAG === "Ok") {
|
|
181
|
+
return {
|
|
182
|
+
TAG: "Ok",
|
|
183
|
+
_0: acc._0.concat([match._0])
|
|
184
|
+
};
|
|
185
|
+
} else {
|
|
186
|
+
return {
|
|
187
|
+
TAG: "Error",
|
|
188
|
+
_0: match._0
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
return acc;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
if (msg$1.TAG !== "Ok") {
|
|
196
|
+
return fail(undefined, msg$1._0);
|
|
197
|
+
}
|
|
198
|
+
let msgs = msg$1._0;
|
|
199
|
+
if (msgs.length === 0) {
|
|
200
|
+
return succeed(requestId, inputJson, []);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
await publishJsons(msgs);
|
|
204
|
+
return succeed(requestId, inputJson, pairs.map(param => param[0]));
|
|
205
|
+
} catch (raw_exn$1) {
|
|
206
|
+
let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
|
|
207
|
+
return fail(undefined, messageOf(exn$1, "publish failed"));
|
|
219
208
|
}
|
|
220
209
|
};
|
|
221
210
|
return {
|
|
@@ -232,6 +221,8 @@ export {
|
|
|
232
221
|
receiveResultToOutcome,
|
|
233
222
|
requestIdOf,
|
|
234
223
|
takeAuditRow,
|
|
224
|
+
callerAdmits,
|
|
225
|
+
doorCaller,
|
|
235
226
|
Make,
|
|
236
227
|
}
|
|
237
228
|
/* auditStatusSchema Not a pure module */
|
|
@@ -1017,12 +1017,20 @@ type platformHooks = {
|
|
|
1017
1017
|
) => unit,
|
|
1018
1018
|
// Phase 2: bind generateCommand inside Output.apply.
|
|
1019
1019
|
mutationBindHook?: (~field: string, ~generateCommand: CommandGenerator.commandGenerator) => unit,
|
|
1020
|
-
// InboundTranslationSlice — phase 1: register SDL + stub.
|
|
1021
|
-
|
|
1020
|
+
// InboundTranslationSlice — phase 1: register SDL + stub. `permission` is the
|
|
1021
|
+
// door's rule (`Plugin_Structure.inboundDoorPermission`); `None` admits nobody.
|
|
1022
|
+
inboundMutationResolverHook?: (
|
|
1023
|
+
~fieldName: string,
|
|
1024
|
+
~externalInputSchema: S.t<unknown>,
|
|
1025
|
+
~permission: Reventless.Authorization.permission=?,
|
|
1026
|
+
) => unit,
|
|
1022
1027
|
// InboundTranslationSlice — phase 2: bind receive.
|
|
1023
1028
|
inboundMutationBindReceiveHook?: (
|
|
1024
1029
|
~fieldName: string,
|
|
1025
|
-
~receive:
|
|
1030
|
+
~receive: (
|
|
1031
|
+
JSON.t,
|
|
1032
|
+
~caller: Reventless.Identity.t=?,
|
|
1033
|
+
) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
|
|
1026
1034
|
) => unit,
|
|
1027
1035
|
// GraphQL type definitions.
|
|
1028
1036
|
schemaTypeRegistrationHook?: array<string> => unit,
|