@reventlessdev/reventless-core 3.0.0-alpha.291 → 3.0.0-alpha.293
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 +21 -0
- package/package.json +7 -7
- package/src/admin/Platform_Admin_Structure.res +1 -2
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
- package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res.mjs +2 -2
- 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 +31 -56
- package/src/components/Dcb/Dcb_Builder.res.mjs +13 -29
- 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 +125 -164
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +121 -130
- package/src/plugin/component/Plugin_Builder.res +8 -18
- package/src/plugin/component/Plugin_Builder.res.mjs +2 -8
- package/src/plugin/component/Plugin_Helpers.res +16 -9
- package/src/plugin/component/Plugin_Helpers.res.mjs +1 -1
- package/src/plugin/component/Plugin_Structure.res +35 -28
- package/src/plugin/component/Plugin_Structure.res.mjs +18 -8
- package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +1 -1
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +1 -1
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +1 -1
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +1 -1
- package/src/plugin/lifecycle/PluginSpec.res.mjs +2 -2
- package/tests/aggregate/AggregateCacheTest.res.mjs +4 -4
- package/tests/aggregate/AggregateConflictTest.res.mjs +4 -4
- package/tests/aggregate/AggregateFixtures.res.mjs +4 -4
- package/tests/aggregate/AggregateSnapshotTest.res.mjs +6 -6
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +6 -6
- package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +3 -3
- package/tests/commandgenerator/OwnerActingTest.res.mjs +7 -7
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +1 -1
- package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +3 -3
- package/tests/dcb/DcbFixtures.res.mjs +2 -2
- package/tests/dcb/DcbStateChangeSliceTest.res.mjs +7 -7
- package/tests/eventmapper/EventMapperFixtures.res.mjs +4 -4
- package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +2 -2
- package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +2 -2
- package/tests/plugin/PluginStructureAccessTest.res +57 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +91 -1
- package/tests/plugin/PluginStructureTest.res.mjs +10 -10
- package/tests/plugin/StateChange/PsAttachInvoice.res.mjs +2 -2
- package/tests/plugin/StateChange/PsChangePhoto.res.mjs +2 -2
- package/tests/plugin/StateChange/PsDispatchShipment.res.mjs +2 -2
- package/tests/plugin/StateChange/PsGatedCommands.res.mjs +3 -3
- package/tests/plugin/StateChange/PsPlaceOrder.res.mjs +2 -2
- package/tests/plugin/StateChange/PsReserveStock.res.mjs +2 -2
- package/tests/plugin/StateChange/PsShipOrder.res.mjs +2 -2
- package/tests/plugin/StateChange/PsTransitionSwitch.res.mjs +2 -2
- package/tests/plugin/StateChange/PsTypoStore.res.mjs +2 -2
- package/tests/plugin/StateChange/PsUploadAvatar.res.mjs +2 -2
- package/tests/plugin/StateChange/PsUploadImages.res.mjs +2 -2
- package/tests/util/CommandPublisherTest.res.mjs +2 -2
|
@@ -55,8 +55,8 @@ function Make(QueryDbStorage) {
|
|
|
55
55
|
let publishJsonsFn = param[1];
|
|
56
56
|
let queryDbOps = param[0];
|
|
57
57
|
return {
|
|
58
|
-
receive: async inputJson => {
|
|
59
|
-
let result = await Callback.receive(publishJsonsFn, inputJson);
|
|
58
|
+
receive: async (inputJson, caller) => {
|
|
59
|
+
let result = await Callback.receive(publishJsonsFn, inputJson, caller);
|
|
60
60
|
await syncToQueryDb(queryDbOps, InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result));
|
|
61
61
|
return result;
|
|
62
62
|
}
|
|
@@ -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,105 @@ 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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if encodeError.contents->Option.isNone {
|
|
160
|
-
try {
|
|
161
|
-
let commandJson = cmd->JSON.stringifyAny->Option.getOrThrow->JSON.parseOrThrow
|
|
162
|
-
let msg: Reventless.Message.commandJson = {
|
|
163
|
-
id: targetId,
|
|
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
|
-
}
|
|
178
|
-
}
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
switch encodeError.contents {
|
|
182
|
-
| Some(msg) =>
|
|
183
|
-
auditLog->Dict.set(
|
|
184
|
-
requestId,
|
|
185
|
-
{
|
|
186
|
-
input: inputJson->JSON.stringify,
|
|
187
|
-
status: Failure,
|
|
188
|
-
error: msg,
|
|
189
|
-
receivedAt: now(),
|
|
190
|
-
},
|
|
171
|
+
switch pairs->Array.reduce(Ok([]), (acc, pair) =>
|
|
172
|
+
switch (acc, encode(pair)) {
|
|
173
|
+
| (Ok(msgs), Ok(msg)) => Ok(msgs->Array.concat([msg]))
|
|
174
|
+
| (Error(_) as failed, _) => failed
|
|
175
|
+
| (_, Error(msg)) => Error(msg)
|
|
176
|
+
}
|
|
177
|
+
) {
|
|
178
|
+
| Error(msg) => fail(msg)
|
|
179
|
+
// Each command against its own rule, read by the name it encodes to; one
|
|
180
|
+
// refused refuses the message.
|
|
181
|
+
| Ok(msgs)
|
|
182
|
+
if msgs->Array.some(({commandJson}) =>
|
|
183
|
+
!callerAdmits(
|
|
184
|
+
commandJson
|
|
185
|
+
->Reventless.Message.variantNameOfJson
|
|
186
|
+
->Spec.authorizationOf
|
|
187
|
+
->Reventless.Authorization.named,
|
|
188
|
+
caller,
|
|
191
189
|
)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
status: Success,
|
|
205
|
-
targetIds,
|
|
206
|
-
commandCount: pairs->Array.length,
|
|
207
|
-
receivedAt: now(),
|
|
208
|
-
},
|
|
209
|
-
)
|
|
210
|
-
Ok({requestId, targetIds, commandCount: pairs->Array.length})
|
|
211
|
-
} 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})
|
|
228
|
-
}
|
|
190
|
+
) =>
|
|
191
|
+
fail(
|
|
192
|
+
~errorCode="Forbidden",
|
|
193
|
+
`${Spec.name}: the caller is not authorized for every command this input translates into`,
|
|
194
|
+
)
|
|
195
|
+
| Ok([]) => succeed(~requestId, ~inputJson, [])
|
|
196
|
+
| Ok(msgs) =>
|
|
197
|
+
try {
|
|
198
|
+
await publishJsons(msgs)
|
|
199
|
+
succeed(~requestId, ~inputJson, pairs->Array.map(((targetId, _)) => targetId))
|
|
200
|
+
} catch {
|
|
201
|
+
| exn => fail(messageOf(exn, "publish failed"))
|
|
229
202
|
}
|
|
230
203
|
}
|
|
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
204
|
}
|
|
244
205
|
}
|
|
245
206
|
}
|
|
@@ -3,11 +3,18 @@
|
|
|
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 Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
12
|
+
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
9
13
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
14
|
+
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
15
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
10
16
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
17
|
+
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
11
18
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
12
19
|
|
|
13
20
|
let auditStatusSchema = Sury.union([
|
|
@@ -49,7 +56,7 @@ function receiveResultToOutcome(result) {
|
|
|
49
56
|
return {
|
|
50
57
|
TAG: "Rejected",
|
|
51
58
|
msgId: match$1.requestId,
|
|
52
|
-
errorCode: "TranslationFailed",
|
|
59
|
+
errorCode: Stdlib_Option.getOr(match$1.errorCode, "TranslationFailed"),
|
|
53
60
|
errorDetail: match$1.error
|
|
54
61
|
};
|
|
55
62
|
}
|
|
@@ -64,158 +71,140 @@ function takeAuditRow(auditLog, requestId) {
|
|
|
64
71
|
return row;
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
function callerAdmits(rule, caller) {
|
|
75
|
+
if (caller === undefined) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
let match = OwnerScope$Reventless.classify(caller, []);
|
|
79
|
+
if (typeof match !== "object") {
|
|
80
|
+
return true;
|
|
81
|
+
} else {
|
|
82
|
+
return Authorization$Reventless.isAllowed(rule, caller);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function doorCaller(identity) {
|
|
87
|
+
return Stdlib_Option.getOr((identity == null) ? undefined : Primitive_option.some(identity), Identity$Reventless.anonymous);
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
function Make(Spec) {
|
|
68
91
|
return Translation => {
|
|
69
92
|
let auditLog = {};
|
|
70
93
|
let makeMeta = () => Message$ReventlessCore.generateMeta(Spec.targetName, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
let
|
|
94
|
+
let messageOf = (exn, fallback) => Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), fallback);
|
|
95
|
+
let succeed = (requestId, inputJson, targetIds) => {
|
|
96
|
+
let commandCount = targetIds.length;
|
|
97
|
+
auditLog[requestId] = {
|
|
98
|
+
input: JSON.stringify(inputJson),
|
|
99
|
+
status: "Success",
|
|
100
|
+
targetIds: targetIds,
|
|
101
|
+
commandCount: commandCount,
|
|
102
|
+
receivedAt: new Date().toISOString()
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
TAG: "Ok",
|
|
106
|
+
_0: {
|
|
107
|
+
requestId: requestId,
|
|
108
|
+
targetIds: targetIds,
|
|
109
|
+
commandCount: commandCount
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
let encode = param => {
|
|
74
114
|
try {
|
|
75
|
-
|
|
115
|
+
return {
|
|
76
116
|
TAG: "Ok",
|
|
77
|
-
_0:
|
|
117
|
+
_0: {
|
|
118
|
+
id: param[0],
|
|
119
|
+
meta: makeMeta(),
|
|
120
|
+
commandJson: Util_Sury$Reventless.toJson(param[1], Spec.commandSchema)
|
|
121
|
+
}
|
|
78
122
|
};
|
|
79
123
|
} catch (raw_exn) {
|
|
80
124
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
81
|
-
|
|
82
|
-
|
|
125
|
+
Effect.runSync(EffectLogger$ReventlessCore.logError(`InboundTranslationSlice(` + Spec.name + `)`, undefined, `failed to encode command: ` + messageOf(exn, "unknown")));
|
|
126
|
+
return {
|
|
83
127
|
TAG: "Error",
|
|
84
|
-
_0:
|
|
128
|
+
_0: "failed to encode command"
|
|
85
129
|
};
|
|
86
130
|
}
|
|
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;
|
|
131
|
+
};
|
|
132
|
+
let receive = async (publishJsons, inputJson, caller) => {
|
|
133
|
+
let requestId = Uuid.v4();
|
|
134
|
+
let fail = (errorCode, error) => {
|
|
206
135
|
auditLog[requestId] = {
|
|
207
136
|
input: JSON.stringify(inputJson),
|
|
208
137
|
status: "Failure",
|
|
209
|
-
error:
|
|
138
|
+
error: error,
|
|
210
139
|
receivedAt: new Date().toISOString()
|
|
211
140
|
};
|
|
212
141
|
return {
|
|
213
142
|
TAG: "Error",
|
|
214
143
|
_0: {
|
|
215
144
|
requestId: requestId,
|
|
216
|
-
error:
|
|
145
|
+
error: error,
|
|
146
|
+
errorCode: errorCode
|
|
217
147
|
}
|
|
218
148
|
};
|
|
149
|
+
};
|
|
150
|
+
let parsed;
|
|
151
|
+
try {
|
|
152
|
+
parsed = {
|
|
153
|
+
TAG: "Ok",
|
|
154
|
+
_0: Sury.parseOrThrow(inputJson, Spec.externalInputSchema)
|
|
155
|
+
};
|
|
156
|
+
} catch (raw_exn) {
|
|
157
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
158
|
+
parsed = {
|
|
159
|
+
TAG: "Error",
|
|
160
|
+
_0: messageOf(exn, "invalid input")
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (parsed.TAG !== "Ok") {
|
|
164
|
+
return fail(undefined, parsed._0);
|
|
165
|
+
}
|
|
166
|
+
let msg = Translation.translate(parsed._0);
|
|
167
|
+
if (msg.TAG !== "Ok") {
|
|
168
|
+
return fail(undefined, msg._0);
|
|
169
|
+
}
|
|
170
|
+
let pairs = msg._0;
|
|
171
|
+
let msg$1 = Stdlib_Array.reduce(pairs, {
|
|
172
|
+
TAG: "Ok",
|
|
173
|
+
_0: []
|
|
174
|
+
}, (acc, pair) => {
|
|
175
|
+
let match = encode(pair);
|
|
176
|
+
if (acc.TAG === "Ok") {
|
|
177
|
+
if (match.TAG === "Ok") {
|
|
178
|
+
return {
|
|
179
|
+
TAG: "Ok",
|
|
180
|
+
_0: acc._0.concat([match._0])
|
|
181
|
+
};
|
|
182
|
+
} else {
|
|
183
|
+
return {
|
|
184
|
+
TAG: "Error",
|
|
185
|
+
_0: match._0
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
return acc;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
if (msg$1.TAG !== "Ok") {
|
|
193
|
+
return fail(undefined, msg$1._0);
|
|
194
|
+
}
|
|
195
|
+
let msgs = msg$1._0;
|
|
196
|
+
if (msgs.some(param => !callerAdmits(Authorization$Reventless.named(Spec.authorizationOf(Message$Reventless.variantNameOfJson(param.commandJson))), caller))) {
|
|
197
|
+
return fail("Forbidden", Spec.name + `: the caller is not authorized for every command this input translates into`);
|
|
198
|
+
}
|
|
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 */
|