@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +6 -6
  3. package/src/admin/Platform_Admin_Structure.res +6 -0
  4. package/src/admin/Platform_Admin_Structure.res.mjs +9 -3
  5. package/src/admin/Platform_BakedManifest.res +154 -0
  6. package/src/admin/Platform_BakedManifest.res.mjs +156 -0
  7. package/src/admin/Platform_ComponentDefinitionsApi.res +9 -2
  8. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +18 -2
  9. package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
  10. package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
  11. package/src/components/Api/QueryDbListQuery.res +25 -1
  12. package/src/components/Api/QueryDbListQuery.res.mjs +10 -3
  13. package/src/components/Api/SuryToJsonSchema.res +19 -0
  14. package/src/components/Api/SuryToJsonSchema.res.mjs +18 -4
  15. package/src/components/CommandGenerator/CommandGenerator_Callback.res +48 -0
  16. package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +27 -1
  17. package/src/plugin/component/Plugin_Structure.res +59 -3
  18. package/src/plugin/component/Plugin_Structure.res.mjs +29 -9
  19. package/tests/admin/Platform_BakedManifestTest.res +244 -0
  20. package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
  21. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +10 -0
  22. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +24 -8
  23. package/tests/admin/Platform_PluginStructuresApiTest.res +2 -0
  24. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +6 -2
  25. package/tests/api/Api_IdsTest.res.mjs +1 -1
  26. package/tests/api/OwnerScopeDiagnosticsTest.res +109 -0
  27. package/tests/api/OwnerScopeDiagnosticsTest.res.mjs +91 -0
  28. package/tests/api/SuryToJsonSchemaTest.res +45 -0
  29. package/tests/api/SuryToJsonSchemaTest.res.mjs +23 -0
  30. package/tests/commandgenerator/OwnerStampingTest.res +174 -0
  31. package/tests/commandgenerator/OwnerStampingTest.res.mjs +195 -0
  32. package/tests/plugin/PluginStructureAccessTest.res +110 -0
  33. package/tests/plugin/PluginStructureAccessTest.res.mjs +144 -0
  34. package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
  35. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
  36. package/tests/plugin/StateViewSlice/PsGatedView.res +15 -0
  37. package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +60 -0
  38. 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
- if (passSearch && passPrefix && passIds) {
99
- return passPerField;
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
- props[fieldName] = withAnnotations;
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(~isAggregate, ~mutationFieldFor, ~parentSchema=commandSchema, v)
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(~isAggregate, ~mutationFieldFor, ~parentSchema=commandSchema, commandSchema)
392
- ->Option.mapOr([], def => [def])
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 toCommandDef = (isAggregate, mutationFieldFor, parentSchema, v) => {
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)),