@reventlessdev/reventless-core 3.0.0-alpha.224 → 3.0.0-alpha.226
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 +22 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin_Structure.res +6 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +9 -3
- package/src/admin/Platform_BakedManifest.res +154 -0
- package/src/admin/Platform_BakedManifest.res.mjs +156 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +9 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +18 -2
- package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
- package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
- package/src/components/Api/QueryDbListQuery.res +25 -1
- package/src/components/Api/QueryDbListQuery.res.mjs +10 -3
- package/src/components/Api/SuryToJsonSchema.res +19 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +18 -4
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +48 -0
- package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +27 -1
- package/src/plugin/component/Plugin_Structure.res +59 -3
- package/src/plugin/component/Plugin_Structure.res.mjs +29 -9
- package/tests/admin/Platform_BakedManifestTest.res +244 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +10 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +24 -8
- package/tests/admin/Platform_PluginStructuresApiTest.res +2 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +6 -2
- package/tests/api/Api_IdsTest.res.mjs +1 -1
- package/tests/api/OwnerScopeDiagnosticsTest.res +109 -0
- package/tests/api/OwnerScopeDiagnosticsTest.res.mjs +91 -0
- package/tests/api/SuryToJsonSchemaTest.res +45 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +23 -0
- package/tests/commandgenerator/OwnerStampingTest.res +174 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +195 -0
- package/tests/plugin/PluginStructureAccessTest.res +110 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +144 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
- package/tests/plugin/StateViewSlice/PsGatedView.res +15 -0
- package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +60 -0
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +4 -0
|
@@ -48,7 +48,7 @@ function buildConnection(pageItems, hasNextPage, hasPreviousPage, cursorValueOf)
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function run(items, argsDict, capability, labelField, decodeLocalIdOpt) {
|
|
51
|
+
function run(items, argsDict, capability, labelField, decodeLocalIdOpt, ownerScope) {
|
|
52
52
|
let decodeLocalId = decodeLocalIdOpt !== undefined ? decodeLocalIdOpt : Api_Ids$ReventlessCore.toLocalId;
|
|
53
53
|
let filterDict = Stdlib_Option.getOr(Stdlib_Option.flatMap(argsDict["filter"], Stdlib_JSON.Decode.object), {});
|
|
54
54
|
let search = Stdlib_Option.flatMap(filterDict["search"], Stdlib_JSON.Decode.string);
|
|
@@ -95,8 +95,15 @@ function run(items, argsDict, capability, labelField, decodeLocalIdOpt) {
|
|
|
95
95
|
passIds = true;
|
|
96
96
|
}
|
|
97
97
|
let passPerField = perFieldChecks.every(check => check(item));
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
let passOwner;
|
|
99
|
+
if (ownerScope !== undefined) {
|
|
100
|
+
let required = ownerScope[1];
|
|
101
|
+
passOwner = Stdlib_Option.mapOr(getFieldString(item, ownerScope[0]), false, v => v === required);
|
|
102
|
+
} else {
|
|
103
|
+
passOwner = true;
|
|
104
|
+
}
|
|
105
|
+
if (passSearch && passPrefix && passIds && passPerField) {
|
|
106
|
+
return passOwner;
|
|
100
107
|
} else {
|
|
101
108
|
return false;
|
|
102
109
|
}
|
|
@@ -188,9 +188,17 @@ and withSemantic = (fieldSchema: JSON.t, sem: Reventless.Semantic.t): JSON.t =>
|
|
|
188
188
|
// through here: nothing validates a read schema. A command schema is the input
|
|
189
189
|
// of a form, and a field listed as required is one the form refuses to submit
|
|
190
190
|
// without — which turned `imageUrl?` into a picture every product had to have.
|
|
191
|
+
//
|
|
192
|
+
// `owners` arrives the same way `optional` does — a list of field names read off
|
|
193
|
+
// the sury schema by the caller — rather than as a case in the IR. The IR is
|
|
194
|
+
// shape-driven, and ownership is not a shape: it says which principal a record
|
|
195
|
+
// belongs to, leaving the field a plain string either way. Threading it here
|
|
196
|
+
// also keeps it available on command variants, which carry no annotation spec
|
|
197
|
+
// for `mergeAnnotations` to read.
|
|
191
198
|
and objectRefToJsonSchema = (
|
|
192
199
|
~annotations: option<Reventless.StateAnnotations.stateAnnotationSpec>=?,
|
|
193
200
|
~optional: array<string>=[],
|
|
201
|
+
~owners: array<string>=[],
|
|
194
202
|
fields: dict<SchemaType.schemaType>,
|
|
195
203
|
): JSON.t => {
|
|
196
204
|
let props = Dict.make()
|
|
@@ -203,6 +211,16 @@ and objectRefToJsonSchema = (
|
|
|
203
211
|
| Some(spec) => mergeAnnotations(baseSchema, fieldName, spec)
|
|
204
212
|
| None => baseSchema
|
|
205
213
|
}
|
|
214
|
+
let withAnnotations = if owners->Array.includes(fieldName) {
|
|
215
|
+
switch withAnnotations->JSON.Decode.object {
|
|
216
|
+
| Some(obj) =>
|
|
217
|
+
obj->Dict.set("x-reventless-owner", JSON.Encode.bool(true))
|
|
218
|
+
JSON.Encode.object(obj)
|
|
219
|
+
| None => withAnnotations
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
withAnnotations
|
|
223
|
+
}
|
|
206
224
|
props->Dict.set(fieldName, withAnnotations)
|
|
207
225
|
if !(optional->Array.includes(fieldName)) {
|
|
208
226
|
required->Array.push(fieldName)
|
|
@@ -227,6 +245,7 @@ let deriveObjectSchema = (schema: S.t<unknown>): JSON.t =>
|
|
|
227
245
|
let objSchema = objectRefToJsonSchema(
|
|
228
246
|
~annotations?,
|
|
229
247
|
~optional=SchemaType.optionalFieldNames(schema),
|
|
248
|
+
~owners=Reventless.Owner.fieldNames(schema),
|
|
230
249
|
fields,
|
|
231
250
|
)
|
|
232
251
|
// Surface component-level hints on the top-level object schema.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
5
6
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
6
7
|
import * as SchemaType$ReventlessCore from "./SchemaType.res.mjs";
|
|
7
8
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
@@ -178,7 +179,7 @@ function fromSchemaType(st) {
|
|
|
178
179
|
]
|
|
179
180
|
]);
|
|
180
181
|
case "ObjectRef" :
|
|
181
|
-
return objectRefToJsonSchema(undefined, undefined, st._1);
|
|
182
|
+
return objectRefToJsonSchema(undefined, undefined, undefined, st._1);
|
|
182
183
|
case "Enum" :
|
|
183
184
|
return Object.fromEntries([
|
|
184
185
|
[
|
|
@@ -222,15 +223,28 @@ function withSemantic(fieldSchema, sem) {
|
|
|
222
223
|
return obj;
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
function objectRefToJsonSchema(annotations, optionalOpt, fields) {
|
|
226
|
+
function objectRefToJsonSchema(annotations, optionalOpt, ownersOpt, fields) {
|
|
226
227
|
let optional = optionalOpt !== undefined ? optionalOpt : [];
|
|
228
|
+
let owners = ownersOpt !== undefined ? ownersOpt : [];
|
|
227
229
|
let props = {};
|
|
228
230
|
let required = [];
|
|
229
231
|
Object.entries(fields).forEach(param => {
|
|
230
232
|
let fieldName = param[0];
|
|
231
233
|
let baseSchema = fromSchemaType(param[1]);
|
|
232
234
|
let withAnnotations = annotations !== undefined ? mergeAnnotations(baseSchema, fieldName, annotations) : baseSchema;
|
|
233
|
-
|
|
235
|
+
let withAnnotations$1;
|
|
236
|
+
if (owners.includes(fieldName)) {
|
|
237
|
+
let obj = Stdlib_JSON.Decode.object(withAnnotations);
|
|
238
|
+
if (obj !== undefined) {
|
|
239
|
+
obj["x-reventless-owner"] = true;
|
|
240
|
+
withAnnotations$1 = obj;
|
|
241
|
+
} else {
|
|
242
|
+
withAnnotations$1 = withAnnotations;
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
withAnnotations$1 = withAnnotations;
|
|
246
|
+
}
|
|
247
|
+
props[fieldName] = withAnnotations$1;
|
|
234
248
|
if (!optional.includes(fieldName)) {
|
|
235
249
|
required.push(fieldName);
|
|
236
250
|
return;
|
|
@@ -264,7 +278,7 @@ function deriveObjectSchema(schema) {
|
|
|
264
278
|
]]);
|
|
265
279
|
}
|
|
266
280
|
let annotations = StateAnnotations$Reventless.getSpec(schema);
|
|
267
|
-
let objSchema = objectRefToJsonSchema(annotations, SchemaType$ReventlessCore.optionalFieldNames(schema), fields);
|
|
281
|
+
let objSchema = objectRefToJsonSchema(annotations, SchemaType$ReventlessCore.optionalFieldNames(schema), Owner$Reventless.fieldNames(schema), fields);
|
|
268
282
|
if (annotations === undefined) {
|
|
269
283
|
return objSchema;
|
|
270
284
|
}
|
|
@@ -30,6 +30,48 @@ module type T = {
|
|
|
30
30
|
let generateCommand: CommandGenerator.commandGenerator
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
Overwrite every `@owner`-marked field of the command being issued with the
|
|
35
|
+
authenticated caller's id.
|
|
36
|
+
|
|
37
|
+
Deliberately here rather than in a transport's resolver. The local GraphQL
|
|
38
|
+
resolver and the AppSync mutation template build the same `payload` and both
|
|
39
|
+
hand it to `makeGenerateCommand`, so this is the only place a single edit is
|
|
40
|
+
true on both — and a rule enforced on one transport and not the other is worse
|
|
41
|
+
than no rule, because it reads as enforced.
|
|
42
|
+
|
|
43
|
+
**Overwrite, not fill-if-absent.** A caller who omits the field and a caller who
|
|
44
|
+
sends someone else's id must produce the same row. Filling only the absent case
|
|
45
|
+
passes every test that does not forge the field, and forging it is the whole
|
|
46
|
+
attack.
|
|
47
|
+
|
|
48
|
+
An exempt caller — the platform's own service traffic, or an operator — is left
|
|
49
|
+
alone, so acting on another principal's behalf stays possible for those who may.
|
|
50
|
+
A caller that cannot be identified is refused outright, but only for commands
|
|
51
|
+
that actually carry an owner: an anonymous caller invoking an `AllowAnonymous`
|
|
52
|
+
command with no owner field has nothing to prove and is not this rule's business.
|
|
53
|
+
*/
|
|
54
|
+
let stampOwnerFields = (
|
|
55
|
+
obj: dict<JSON.t>,
|
|
56
|
+
~commandSchema: S.t<unknown>,
|
|
57
|
+
~command: string,
|
|
58
|
+
~identity: Reventless.Identity.t,
|
|
59
|
+
~serviceName: string,
|
|
60
|
+
) =>
|
|
61
|
+
switch Reventless.Owner.variantFieldNames(commandSchema, ~variant=command) {
|
|
62
|
+
| [] => ()
|
|
63
|
+
| ownerFields =>
|
|
64
|
+
switch identity->Reventless.OwnerScope.resolve {
|
|
65
|
+
| System | Elevated(_) => ()
|
|
66
|
+
| Owned({userId}) =>
|
|
67
|
+
ownerFields->Array.forEach(field => obj->Dict.set(field, JSON.Encode.string(userId)))
|
|
68
|
+
| Unidentified(why) =>
|
|
69
|
+
JsError.throwWithMessage(
|
|
70
|
+
`Forbidden: ${serviceName}.${command} records an owner, but the caller could not be identified (${why})`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
33
75
|
let makeGenerateCommand = (
|
|
34
76
|
~publishJsons: CommandGenerator.publishJsons,
|
|
35
77
|
~publishJsonsAndWait: option<CommandTopic.publishJsonsAndWait>=?,
|
|
@@ -58,6 +100,12 @@ let makeGenerateCommand = (
|
|
|
58
100
|
if stripIdFromParams {
|
|
59
101
|
obj->Dict.delete("id")
|
|
60
102
|
}
|
|
103
|
+
obj->stampOwnerFields(
|
|
104
|
+
~commandSchema,
|
|
105
|
+
~command=payload.command,
|
|
106
|
+
~identity=payload.identity,
|
|
107
|
+
~serviceName,
|
|
108
|
+
)
|
|
61
109
|
obj->Dict.toArray
|
|
62
110
|
}
|
|
63
111
|
| None =>
|
|
@@ -5,8 +5,10 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
|
+
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
8
9
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
9
10
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
11
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
10
12
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
11
13
|
import * as LogFormat$ReventlessCore from "../../util/LogFormat.res.mjs";
|
|
12
14
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
@@ -24,6 +26,29 @@ function clearCommandInterceptor() {
|
|
|
24
26
|
commandInterceptorHook.contents = undefined;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
function stampOwnerFields(obj, commandSchema, command, identity, serviceName) {
|
|
30
|
+
let ownerFields = Owner$Reventless.variantFieldNames(commandSchema, command);
|
|
31
|
+
if (ownerFields.length === 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let why = OwnerScope$Reventless.resolve(identity, undefined);
|
|
35
|
+
if (typeof why !== "object") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
switch (why.TAG) {
|
|
39
|
+
case "Elevated" :
|
|
40
|
+
return;
|
|
41
|
+
case "Owned" :
|
|
42
|
+
let userId = why.userId;
|
|
43
|
+
ownerFields.forEach(field => {
|
|
44
|
+
obj[field] = userId;
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
case "Unidentified" :
|
|
48
|
+
return Stdlib_JsError.throwWithMessage(`Forbidden: ` + serviceName + `.` + command + ` records an owner, but the caller could not be identified (` + why._0 + `)`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, commandSchema, componentKind, $staropt$star) {
|
|
28
53
|
return payload => {
|
|
29
54
|
let stripIdFromParams = $staropt$star !== undefined ? $staropt$star : true;
|
|
@@ -41,7 +66,7 @@ function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, com
|
|
|
41
66
|
correlationId: msgId
|
|
42
67
|
};
|
|
43
68
|
let obj = Stdlib_Option.flatMap(JSON.stringify(payload.arguments), jsonString => Stdlib_JSON.Decode.object(JSON.parse(jsonString)));
|
|
44
|
-
let params = obj !== undefined ? (stripIdFromParams ? Stdlib_Dict.$$delete(obj, "id") : undefined, Object.entries(obj)) : Stdlib_JsError.throwWithMessage("Couldn't decode:" + Stdlib_Option.getOr(JSON.stringify(payload.arguments), "<payload.arguments>"));
|
|
69
|
+
let params = obj !== undefined ? (stripIdFromParams ? Stdlib_Dict.$$delete(obj, "id") : undefined, stampOwnerFields(obj, commandSchema, payload.command, payload.identity, serviceName), Object.entries(obj)) : Stdlib_JsError.throwWithMessage("Couldn't decode:" + Stdlib_Option.getOr(JSON.stringify(payload.arguments), "<payload.arguments>"));
|
|
45
70
|
let commandStr = payload.command;
|
|
46
71
|
let match = params.length;
|
|
47
72
|
let commandJson = match !== 0 ? Object.fromEntries([[
|
|
@@ -155,6 +180,7 @@ export {
|
|
|
155
180
|
commandInterceptorHook,
|
|
156
181
|
registerCommandInterceptor,
|
|
157
182
|
clearCommandInterceptor,
|
|
183
|
+
stampOwnerFields,
|
|
158
184
|
makeGenerateCommand,
|
|
159
185
|
Make,
|
|
160
186
|
}
|
|
@@ -299,10 +299,33 @@ let make = (
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
// A rule the server enforces, expressed as the keys a client checks against
|
|
303
|
+
// `identity.groups ++ config.accessTiers`. `AllowGroups` is satisfied by ANY of
|
|
304
|
+
// its groups (`Authorization.isAllowed` is `some`, not `every`), so the array is
|
|
305
|
+
// an any-of and a client must read it that way.
|
|
306
|
+
//
|
|
307
|
+
// `AllowAuthenticated` / `AllowAnonymous` ask for nothing a client can check —
|
|
308
|
+
// anyone holding a session already satisfies them — so they publish no keys
|
|
309
|
+
// rather than a key everyone holds.
|
|
310
|
+
//
|
|
311
|
+
// `DenyAll` also publishes none, deliberately. An unsatisfiable key would render
|
|
312
|
+
// as locked-with-upsell in a tiered shell: a surface advertised as purchasable
|
|
313
|
+
// that no purchase unlocks. A component nobody may call belongs in no menu, and
|
|
314
|
+
// that is an omission for the enumerating side to make, not a key to invent here.
|
|
315
|
+
let accessKeysFor = (rule: Reventless.Authorization.permission): option<array<string>> =>
|
|
316
|
+
switch rule {
|
|
317
|
+
| AllowGroups(groups) if groups->Array.length > 0 => Some(groups)
|
|
318
|
+
| AllowGroups(_) | AllowAuthenticated | AllowAnonymous | DenyAll => None
|
|
319
|
+
}
|
|
320
|
+
|
|
302
321
|
let toCommandDef = (
|
|
303
322
|
~isAggregate,
|
|
304
323
|
~mutationFieldFor: string => string,
|
|
305
324
|
~parentSchema: S.t<unknown>,
|
|
325
|
+
// The PPX-generated `command => permission`. Per VARIANT, not per component:
|
|
326
|
+
// one aggregate carries commands with very different audiences, and a
|
|
327
|
+
// component-level shortcut would gate `AddProduct` and `PlaceOrder` alike.
|
|
328
|
+
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
306
329
|
v: S.t<unknown>,
|
|
307
330
|
): option<Reventless.Plugin.commandDef> => {
|
|
308
331
|
// Build a commandDef for one variant. `properties` is the variant's field dict —
|
|
@@ -329,6 +352,14 @@ let make = (
|
|
|
329
352
|
| Some(excluded) => !(excluded->Set.has(variantName))
|
|
330
353
|
| None => true
|
|
331
354
|
}
|
|
355
|
+
// Evaluated against a synthetic value per constructor, the same shape the
|
|
356
|
+
// resolver builds at call time: a payload-bearing variant compiles to
|
|
357
|
+
// `{TAG, ...}`, a payload-less one to a bare string.
|
|
358
|
+
let syntheticCommand: unknown =
|
|
359
|
+
Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
|
|
360
|
+
? {"TAG": variantName}->Obj.magic
|
|
361
|
+
: variantName->Obj.magic
|
|
362
|
+
let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand))
|
|
332
363
|
({
|
|
333
364
|
Reventless.Plugin.name: variantName,
|
|
334
365
|
// The derived schema, not sury's raw one: `S.toJSONSchema` carries the
|
|
@@ -355,6 +386,11 @@ let make = (
|
|
|
355
386
|
allowedStates,
|
|
356
387
|
targetState,
|
|
357
388
|
apiExposed: Some(apiExposed),
|
|
389
|
+
requiredAccess,
|
|
390
|
+
// Resolved from this constructor's own properties, not the union's: two
|
|
391
|
+
// commands in one slice can disagree about whether they record an owner,
|
|
392
|
+
// and the write path stamps per constructor for the same reason.
|
|
393
|
+
ownerField: Reventless.Owner.fieldNamesOfProperties(properties)->Array.get(0),
|
|
358
394
|
}: Reventless.Plugin.commandDef)
|
|
359
395
|
}
|
|
360
396
|
switch v {
|
|
@@ -379,17 +415,29 @@ let make = (
|
|
|
379
415
|
let extractCommandDefs = (
|
|
380
416
|
~isAggregate,
|
|
381
417
|
~mutationFieldFor: string => string,
|
|
418
|
+
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
382
419
|
commandSchema: S.t<unknown>,
|
|
383
420
|
): array<Reventless.Plugin.commandDef> =>
|
|
384
421
|
switch commandSchema {
|
|
385
422
|
| Union({anyOf}) =>
|
|
386
423
|
anyOf->Array.filterMap(v =>
|
|
387
|
-
toCommandDef(
|
|
424
|
+
toCommandDef(
|
|
425
|
+
~isAggregate,
|
|
426
|
+
~mutationFieldFor,
|
|
427
|
+
~parentSchema=commandSchema,
|
|
428
|
+
~commandAuthorization,
|
|
429
|
+
v,
|
|
430
|
+
)
|
|
388
431
|
)
|
|
389
432
|
| _ =>
|
|
390
433
|
// Single-variant command types compile to a bare Object schema, not a Union.
|
|
391
|
-
toCommandDef(
|
|
392
|
-
|
|
434
|
+
toCommandDef(
|
|
435
|
+
~isAggregate,
|
|
436
|
+
~mutationFieldFor,
|
|
437
|
+
~parentSchema=commandSchema,
|
|
438
|
+
~commandAuthorization,
|
|
439
|
+
commandSchema,
|
|
440
|
+
)->Option.mapOr([], def => [def])
|
|
393
441
|
}
|
|
394
442
|
|
|
395
443
|
// ── Per-component event type extraction ────────────────────────────────────
|
|
@@ -671,6 +719,9 @@ let make = (
|
|
|
671
719
|
searchableFields: label.searchableFields,
|
|
672
720
|
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
673
721
|
statusField: statusFieldFromStateSchema(~entityName=R.Spec.name, stateSchema),
|
|
722
|
+
// Same schema `statusField` reads, so the two cannot disagree about which
|
|
723
|
+
// fields this view has.
|
|
724
|
+
ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
|
|
674
725
|
visibility: visibilityTag(R.Spec.visibility),
|
|
675
726
|
chapter: chapterOf(R.Spec.name),
|
|
676
727
|
// Taken from the `qf` record, never re-derived: `Api_Naming` is the only
|
|
@@ -679,6 +730,7 @@ let make = (
|
|
|
679
730
|
singleQueryField: Some(qf.singleFieldName),
|
|
680
731
|
idField: keyField->Option.map(((f, _)) => f),
|
|
681
732
|
idFieldSource: keyField->Option.map(((_, rung)) => rung),
|
|
733
|
+
requiredAccess: accessKeysFor(R.Spec.authorization),
|
|
682
734
|
}: Reventless.Plugin.queryableDef)
|
|
683
735
|
})
|
|
684
736
|
|
|
@@ -702,11 +754,13 @@ let make = (
|
|
|
702
754
|
searchableFields: label.searchableFields,
|
|
703
755
|
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
704
756
|
statusField: statusFieldFromStateSchema(~entityName=SVS.Spec.name, stateSchema),
|
|
757
|
+
ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
|
|
705
758
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
706
759
|
chapter: chapterOf(SVS.Spec.name),
|
|
707
760
|
singleQueryField: Some(qf.singleFieldName),
|
|
708
761
|
idField: keyField->Option.map(((f, _)) => f),
|
|
709
762
|
idFieldSource: keyField->Option.map(((_, rung)) => rung),
|
|
763
|
+
requiredAccess: accessKeysFor(SVS.Spec.authorization),
|
|
710
764
|
}: Reventless.Plugin.queryableDef)
|
|
711
765
|
})
|
|
712
766
|
|
|
@@ -727,6 +781,7 @@ let make = (
|
|
|
727
781
|
~commandSchema=SCS.Spec.commandSchema->S.castToUnknown,
|
|
728
782
|
~variant=variantName,
|
|
729
783
|
),
|
|
784
|
+
~commandAuthorization=SCS.Spec.commandAuthorization->Obj.magic,
|
|
730
785
|
SCS.Spec.commandSchema->S.castToUnknown,
|
|
731
786
|
),
|
|
732
787
|
producedEventTypes: produced,
|
|
@@ -750,6 +805,7 @@ let make = (
|
|
|
750
805
|
commands: extractCommandDefs(
|
|
751
806
|
~isAggregate=true,
|
|
752
807
|
~mutationFieldFor=variantName => Api_Naming.aggregateMutationField(~plugin=name, ~aggregate=A.Spec.name, ~command=variantName),
|
|
808
|
+
~commandAuthorization=A.Spec.commandAuthorization->Obj.magic,
|
|
753
809
|
A.Spec.commandSchema->S.castToUnknown,
|
|
754
810
|
),
|
|
755
811
|
producedEventTypes: produced,
|
|
@@ -4,6 +4,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
4
4
|
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
7
|
+
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
7
8
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
9
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
9
10
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
@@ -269,7 +270,16 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
269
270
|
];
|
|
270
271
|
}
|
|
271
272
|
};
|
|
272
|
-
let
|
|
273
|
+
let accessKeysFor = rule => {
|
|
274
|
+
if (typeof rule !== "object") {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
let groups = rule._0;
|
|
278
|
+
if (groups.length !== 0) {
|
|
279
|
+
return groups;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
let toCommandDef = (isAggregate, mutationFieldFor, parentSchema, commandAuthorization, v) => {
|
|
273
283
|
let mkDef = (variantName, properties) => {
|
|
274
284
|
let match = commandLevelAndId(isAggregate, variantName, properties);
|
|
275
285
|
let references = extractReferences(properties);
|
|
@@ -280,6 +290,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
280
290
|
let excluded = ApiNoApiHelpers$ReventlessCore.getExcludedVariants(parentSchema);
|
|
281
291
|
apiExposed = excluded !== undefined ? !excluded.has(variantName) : true;
|
|
282
292
|
}
|
|
293
|
+
let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
|
|
294
|
+
TAG: variantName
|
|
295
|
+
}) : variantName;
|
|
296
|
+
let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand));
|
|
283
297
|
return {
|
|
284
298
|
name: variantName,
|
|
285
299
|
schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
|
|
@@ -289,7 +303,9 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
289
303
|
references: references,
|
|
290
304
|
allowedStates: allowedStates,
|
|
291
305
|
targetState: targetState,
|
|
292
|
-
apiExposed: apiExposed
|
|
306
|
+
apiExposed: apiExposed,
|
|
307
|
+
requiredAccess: requiredAccess,
|
|
308
|
+
ownerField: Owner$Reventless.fieldNamesOfProperties(properties)[0]
|
|
293
309
|
};
|
|
294
310
|
};
|
|
295
311
|
switch (v.type) {
|
|
@@ -315,11 +331,11 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
315
331
|
return;
|
|
316
332
|
}
|
|
317
333
|
};
|
|
318
|
-
let extractCommandDefs = (isAggregate, mutationFieldFor, commandSchema) => {
|
|
334
|
+
let extractCommandDefs = (isAggregate, mutationFieldFor, commandAuthorization, commandSchema) => {
|
|
319
335
|
if (commandSchema.type === "union") {
|
|
320
|
-
return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, v));
|
|
336
|
+
return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, v));
|
|
321
337
|
} else {
|
|
322
|
-
return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandSchema), [], def => [def]);
|
|
338
|
+
return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandSchema), [], def => [def]);
|
|
323
339
|
}
|
|
324
340
|
};
|
|
325
341
|
let scsProduced = stateChangeSlices.map(SCS => [
|
|
@@ -513,11 +529,13 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
513
529
|
searchableFields: label.searchableFields,
|
|
514
530
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
515
531
|
statusField: statusFieldFromStateSchema(R.Spec.name, stateSchema),
|
|
532
|
+
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
516
533
|
visibility: visibilityTag(R.Spec.visibility),
|
|
517
534
|
chapter: componentChapters[R.Spec.name],
|
|
518
535
|
singleQueryField: qf.singleFieldName,
|
|
519
536
|
idField: Stdlib_Option.map(keyField, param => param[0]),
|
|
520
|
-
idFieldSource: Stdlib_Option.map(keyField, param => param[1])
|
|
537
|
+
idFieldSource: Stdlib_Option.map(keyField, param => param[1]),
|
|
538
|
+
requiredAccess: accessKeysFor(R.Spec.authorization)
|
|
521
539
|
};
|
|
522
540
|
});
|
|
523
541
|
let stateViewDefs = stateViewSlices.map((SVS, i) => {
|
|
@@ -537,11 +555,13 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
537
555
|
searchableFields: label.searchableFields,
|
|
538
556
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
539
557
|
statusField: statusFieldFromStateSchema(SVS.Spec.name, stateSchema),
|
|
558
|
+
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
540
559
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
541
560
|
chapter: componentChapters[SVS.Spec.name],
|
|
542
561
|
singleQueryField: qf.singleFieldName,
|
|
543
562
|
idField: Stdlib_Option.map(keyField, param => param[0]),
|
|
544
|
-
idFieldSource: Stdlib_Option.map(keyField, param => param[1])
|
|
563
|
+
idFieldSource: Stdlib_Option.map(keyField, param => param[1]),
|
|
564
|
+
requiredAccess: accessKeysFor(SVS.Spec.authorization)
|
|
545
565
|
};
|
|
546
566
|
});
|
|
547
567
|
let stateChangeDefs = stateChangeSlices.map((SCS, i) => {
|
|
@@ -551,7 +571,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
551
571
|
let consumed = match$1[1];
|
|
552
572
|
return {
|
|
553
573
|
name: SCS.Spec.name,
|
|
554
|
-
commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandSchema),
|
|
574
|
+
commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandSchema),
|
|
555
575
|
producedEventTypes: produced,
|
|
556
576
|
consumedEventTypes: consumed,
|
|
557
577
|
linkedViews: linkedSvsFor(produced),
|
|
@@ -566,7 +586,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
566
586
|
let produced = match[1];
|
|
567
587
|
return {
|
|
568
588
|
name: A.Spec.name,
|
|
569
|
-
commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandSchema),
|
|
589
|
+
commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandSchema),
|
|
570
590
|
producedEventTypes: produced,
|
|
571
591
|
consumedEventTypes: [],
|
|
572
592
|
linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
|