@reventlessdev/reventless-core 3.0.0-alpha.284 → 3.0.0-alpha.286

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 (58) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +7 -7
  3. package/src/Message.res.mjs +3 -0
  4. package/src/Roles.res +3 -0
  5. package/src/Roles.res.mjs +2 -0
  6. package/src/admin/Platform_AdminGroup.res +12 -0
  7. package/src/admin/Platform_AdminGroup.res.mjs +23 -0
  8. package/src/admin/Platform_Admin_Structure.res +14 -10
  9. package/src/admin/Platform_Admin_Structure.res.mjs +22 -33
  10. package/src/components/Aggregate/Aggregate_Callback.res +116 -39
  11. package/src/components/Aggregate/Aggregate_Callback.res.mjs +96 -37
  12. package/src/components/AutomationSlice/AutomationSlice_Builder.res +2 -1
  13. package/src/components/CommandGenerator/CommandGenerator_Callback.res +9 -8
  14. package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +10 -8
  15. package/src/components/CommandTopic/CommandTopic.res.mjs +3 -0
  16. package/src/components/CommandTopic/CommandTopic_Helpers.res +37 -1
  17. package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +29 -0
  18. package/src/components/Counter/Counter_Builder.res +4 -2
  19. package/src/components/Dcb/Dcb_Builder.res +9 -6
  20. package/src/components/Dcb/Dcb_Builder.res.mjs +6 -6
  21. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +2 -1
  22. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +2 -1
  23. package/src/components/QueryDb/QueryDb_Builder.res +1 -1
  24. package/src/components/QueryDb/QueryDb_Builder.res.mjs +3 -2
  25. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +191 -141
  26. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +66 -12
  27. package/src/components/StateViewSlice/StateViewSlice_Builder.res +1 -0
  28. package/src/plugin/api/PluginBaseFragment.res +1 -1
  29. package/src/plugin/api/PluginBaseFragment.res.mjs +2 -1
  30. package/src/plugin/component/Plugin_Builder.res +7 -3
  31. package/src/plugin/component/Plugin_Builder.res.mjs +6 -3
  32. package/src/plugin/component/Plugin_Helpers.res +35 -6
  33. package/src/plugin/component/Plugin_Helpers.res.mjs +47 -5
  34. package/src/plugin/component/Plugin_Structure.res +52 -19
  35. package/src/plugin/component/Plugin_Structure.res.mjs +40 -15
  36. package/src/plugin/lifecycle/PluginsReadModelSpec.res +1 -1
  37. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +1 -1
  38. package/tests/admin/AdminApiSchemaDriftTest.res +9 -3
  39. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +3 -3
  40. package/tests/admin/Platform_Admin_StructureTest.res +17 -14
  41. package/tests/admin/Platform_Admin_StructureTest.res.mjs +31 -19
  42. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +8 -72
  43. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +9 -39
  44. package/tests/api/GraphQL_FragmentGeneratorTest.res +1 -1
  45. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +1 -1
  46. package/tests/commandgenerator/OwnerActingTest.res +601 -0
  47. package/tests/commandgenerator/OwnerActingTest.res.mjs +1018 -0
  48. package/tests/commandgenerator/OwnerStampingTest.res +90 -0
  49. package/tests/commandgenerator/OwnerStampingTest.res.mjs +87 -0
  50. package/tests/commandtopic/CommandOutcomeHookTest.res +1 -0
  51. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +11 -1
  52. package/tests/plugin/PluginStructureAccessTest.res +30 -1
  53. package/tests/plugin/PluginStructureAccessTest.res.mjs +28 -1
  54. package/tests/plugin/StateChange/PsGatedCommands.res +9 -1
  55. package/tests/plugin/StateChange/PsGatedCommands.res.mjs +4 -1
  56. package/tests/plugin/StateView/PsGatedView.res +1 -1
  57. package/tests/plugin/StateView/PsGatedView.res.mjs +1 -1
  58. package/tests/plugin/pluginDefinitionRequiredScalars.txt +5 -0
@@ -14,7 +14,9 @@ import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
14
14
  import * as Plugin$ReventlessCore from "./Plugin.res.mjs";
15
15
  import * as StackReference$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/StackReference.res.mjs";
16
16
  import * as Adapter$ReventlessCore from "../../adapter/Adapter.res.mjs";
17
+ import * as RoleCoverage$Reventless from "@reventlessdev/reventless-spec/src/types/RoleCoverage.res.mjs";
17
18
  import * as Aggregate$ReventlessCore from "../../components/Aggregate/Aggregate.res.mjs";
19
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
18
20
  import * as Component$ReventlessCore from "../../components/Component.res.mjs";
19
21
  import * as ReadModel$ReventlessCore from "../../components/ReadModel/ReadModel.res.mjs";
20
22
  import * as Api_Naming$ReventlessCore from "../../components/Api/Api_Naming.res.mjs";
@@ -124,6 +126,7 @@ function Make(Spec) {
124
126
  let systemCallableComponents = systemCallableComponentsOpt !== undefined ? systemCallableComponentsOpt : [];
125
127
  let componentRuntime = componentRuntimeOpt !== undefined ? componentRuntimeOpt : ({});
126
128
  PluginRuntimeBuilder.registerPluginName(name);
129
+ Stdlib_Option.forEach(pluginStructure, s => RoleCoverage$Reventless.check(s, name));
127
130
  let version = PackageVersion$Reventless.fromCaller();
128
131
  return Component$ReventlessCore.make(ComponentType$ReventlessCore.toString(Plugin$ReventlessCore.componentType), name, (extra, extra$1) => {
129
132
  let _prevPluginName = Logger$ReventlessCore.currentPluginName.contents;
@@ -173,7 +176,7 @@ function Make(Spec) {
173
176
  if (fieldNames.length === 0) {
174
177
  return [];
175
178
  }
176
- Stdlib_Option.forEach(Spec.hooks.mutationResolverHook, registerResolver => registerResolver("Aggregate", fieldNames, commandSchema, M.Spec.commandAuthorization));
179
+ Stdlib_Option.forEach(Spec.hooks.mutationResolverHook, registerResolver => registerResolver("Aggregate", fieldNames, commandSchema, command => Authorization$Reventless.named(M.Spec.commandAuthorization(command))));
177
180
  let aggDef = Stdlib_Option.flatMap(pluginStructure, s => s.aggregates.find(d => d.name === M.Spec.name));
178
181
  let fieldPermissions = {};
179
182
  filteredConstructorNames.forEach((cname, idx) => {
@@ -182,7 +185,7 @@ function Make(Spec) {
182
185
  let syntheticCmd = hasPayload ? ({
183
186
  TAG: cname
184
187
  }) : cname;
185
- let rule = M.Spec.commandAuthorization(syntheticCmd);
188
+ let rule = Authorization$Reventless.named(M.Spec.commandAuthorization(syntheticCmd));
186
189
  fieldPermissions[fieldName] = rule;
187
190
  });
188
191
  return [{
@@ -205,7 +208,7 @@ function Make(Spec) {
205
208
  returnTypeName: qn.returnTypeName,
206
209
  stateSchema: R.Spec.stateSchema,
207
210
  authorization: undefined,
208
- permission: R.Spec.authorization,
211
+ permission: Authorization$Reventless.named(R.Spec.authorization),
209
212
  connectionSpec: true,
210
213
  subIdField: subIdField,
211
214
  indexQueries: indexes.length !== 0 ? indexes : undefined,
@@ -783,6 +783,15 @@ type pluginDeployedInfo = {
783
783
  deployedAt: string,
784
784
  actor: string,
785
785
  deploymentId: string,
786
+ /** The commit the deployment was built from, when CI or git could say. */
787
+ commit?: string,
788
+ /** The working tree had changes the commit does not hold. */
789
+ dirty?: bool,
790
+ /** What the environment is for, from the app's `environments.yaml`. */
791
+ purpose?: string,
792
+ tag?: string,
793
+ /** The pull request a review environment was deployed for. */
794
+ pullRequest?: int,
786
795
  kind?: pluginKind,
787
796
  displayName?: string,
788
797
  vendor?: string,
@@ -1141,7 +1150,8 @@ let registerAdminAggregateMutations = (
1141
1150
  ~kind=Aggregate,
1142
1151
  ~fields=fieldNames,
1143
1152
  ~commandSchema,
1144
- ~commandAuthorization=M.Spec.commandAuthorization->Obj.magic,
1153
+ ~commandAuthorization=command =>
1154
+ M.Spec.commandAuthorization(command->Obj.magic)->Reventless.Authorization.named,
1145
1155
  )
1146
1156
  )
1147
1157
  }
@@ -1237,21 +1247,34 @@ let getInteropMeta = (): Pulumi.Output.t<JSON.t> => {
1237
1247
  }
1238
1248
 
1239
1249
  // ---------------------------------------------------------------------------
1240
- // Stack metadata export — environment, region, timestamp, actor, git SHA.
1250
+ // Stack metadata export — environment, region, timestamp, actor, commit.
1241
1251
  // Called from both exportPluginOutputs and exportPlatformOutputs.
1242
1252
  // ---------------------------------------------------------------------------
1243
1253
  let exportDeploymentMetadata = () => {
1254
+ let provenance = Reventless.DeploymentProvenance.current()
1255
+ let stack = Pulumi.Pulumi.getStackName()
1256
+ let optional =
1257
+ [
1258
+ ("commit", provenance.commit),
1259
+ ("purpose", provenance.purpose),
1260
+ ("tag", provenance.tag),
1261
+ ("pullRequest", provenance.pullRequest->Option.map(n => n->Int.toString)),
1262
+ ]->Array.filterMap(((key, value)) => value->Option.map(v => (key, v)))
1244
1263
  let metadata =
1245
1264
  [
1246
- ("environment", Pulumi.Pulumi.getStackName()),
1265
+ ("environment", provenance.environment->Option.getOr(stack)),
1266
+ ("stack", stack),
1247
1267
  (
1248
1268
  "region",
1249
1269
  Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region")->Option.getOr("unknown"),
1250
1270
  ),
1251
1271
  ("timestamp", Date.make()->Date.toISOString),
1252
- ("gitSha", NodeProcess.env->Dict.get("GITHUB_SHA")->Option.getOr("unknown")),
1272
+ ("gitSha", provenance.commit->Option.getOr("unknown")),
1273
+ ("dirty", provenance.dirty ? "true" : "false"),
1253
1274
  ("actor", NodeProcess.env->Dict.get("GITHUB_ACTOR")->Option.getOr("unknown")),
1254
- ]->Dict.fromArray
1275
+ ]
1276
+ ->Array.concat(optional)
1277
+ ->Dict.fromArray
1255
1278
  Pulumi.Pulumi.export(
1256
1279
  "deploymentMetadata",
1257
1280
  metadata->Dict.mapValues(JSON.Encode.string)->JSON.Encode.object->Pulumi.Output.make,
@@ -1595,6 +1618,7 @@ let exportPluginOutputs = (pluginOutputs: Plugin.outputs) => {
1595
1618
  ->Dict.get("GITHUB_SHA")
1596
1619
  ->Option.orElse(NodeProcess.env->Dict.get("CI_COMMIT_SHA"))
1597
1620
  ->Option.getOr(Date.make()->Date.toISOString)
1621
+ let provenance = Reventless.DeploymentProvenance.current()
1598
1622
  let schemaFor = name => componentSchemaRegistry->Dict.get(name)->Option.getOr({})
1599
1623
  let resolveAggregates = pluginOutputs.aggregates->Pulumi.Output.flatMap(aggs =>
1600
1624
  aggs
@@ -1855,11 +1879,16 @@ let exportPluginOutputs = (pluginOutputs: Plugin.outputs) => {
1855
1879
  let info: pluginDeployedInfo = {
1856
1880
  name,
1857
1881
  version,
1858
- environment: Pulumi.Pulumi.getStackName(),
1882
+ environment: provenance.environment->Option.getOr(Pulumi.Pulumi.getStackName()),
1859
1883
  stackName: Pulumi.Pulumi.getStackName(),
1860
1884
  deployedAt: Date.make()->Date.toISOString,
1861
1885
  actor,
1862
1886
  deploymentId,
1887
+ commit: ?provenance.commit,
1888
+ dirty: ?(provenance.dirty ? Some(true) : None),
1889
+ purpose: ?provenance.purpose,
1890
+ tag: ?provenance.tag,
1891
+ pullRequest: ?provenance.pullRequest,
1863
1892
  kind: ?(meta->Option.flatMap(m => m.kind)),
1864
1893
  displayName: ?(meta->Option.flatMap(m => m.displayName)),
1865
1894
  vendor: ?(meta->Option.flatMap(m => m.vendor)),
@@ -21,6 +21,7 @@ import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
21
21
  import * as Adapter$ReventlessCore from "../../adapter/Adapter.res.mjs";
22
22
  import * as Task$ReventlessInterop from "@reventlessdev/reventless-interop/src/components/Task.res.mjs";
23
23
  import * as Aggregate$ReventlessCore from "../../components/Aggregate/Aggregate.res.mjs";
24
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
24
25
  import * as Component$ReventlessCore from "../../components/Component.res.mjs";
25
26
  import * as ReadModel$ReventlessCore from "../../components/ReadModel/ReadModel.res.mjs";
26
27
  import * as Api_Naming$ReventlessCore from "../../components/Api/Api_Naming.res.mjs";
@@ -42,6 +43,7 @@ import * as ApiNoApiHelpers$ReventlessCore from "../../components/Api/ApiNoApiHe
42
43
  import * as AutomationSlice$ReventlessCore from "../../components/AutomationSlice/AutomationSlice.res.mjs";
43
44
  import * as Builder_Helpers$ReventlessCore from "../../components/Builder_Helpers.res.mjs";
44
45
  import * as Plugin_Callback$ReventlessCore from "./Plugin_Callback.res.mjs";
46
+ import * as DeploymentProvenance$Reventless from "@reventlessdev/reventless-spec/src/types/DeploymentProvenance.res.mjs";
45
47
  import * as Plugin_BuiltHook$ReventlessCore from "./Plugin_BuiltHook.res.mjs";
46
48
  import * as StateChangeSlice$ReventlessCore from "../../components/StateChangeSlice/StateChangeSlice.res.mjs";
47
49
  import * as ExtensionPoint$ReventlessInterop from "@reventlessdev/reventless-interop/src/components/ExtensionPoint.res.mjs";
@@ -539,7 +541,7 @@ function registerAdminAggregateMutations(aggregates, hooks) {
539
541
  let fieldNames = filteredConstructorNames.map(cname => Api_Naming$ReventlessCore.adminField(M.Spec.name + "_" + cname));
540
542
  aggregateMutationFieldsRegistry[M.Spec.name] = fieldNames;
541
543
  if (fieldNames.length !== 0) {
542
- return Stdlib_Option.forEach(hooks.mutationResolverHook, registerResolver => registerResolver("Aggregate", fieldNames, commandSchema, M.Spec.commandAuthorization));
544
+ return Stdlib_Option.forEach(hooks.mutationResolverHook, registerResolver => registerResolver("Aggregate", fieldNames, commandSchema, command => Authorization$Reventless.named(M.Spec.commandAuthorization(command))));
543
545
  }
544
546
  });
545
547
  }
@@ -607,10 +609,40 @@ function getInteropMeta() {
607
609
  }
608
610
 
609
611
  function exportDeploymentMetadata() {
612
+ let provenance = DeploymentProvenance$Reventless.current();
613
+ let stack = Pulumi.getStack();
614
+ let optional = Stdlib_Array.filterMap([
615
+ [
616
+ "commit",
617
+ provenance.commit
618
+ ],
619
+ [
620
+ "purpose",
621
+ provenance.purpose
622
+ ],
623
+ [
624
+ "tag",
625
+ provenance.tag
626
+ ],
627
+ [
628
+ "pullRequest",
629
+ Stdlib_Option.map(provenance.pullRequest, n => n.toString())
630
+ ]
631
+ ], param => {
632
+ let key = param[0];
633
+ return Stdlib_Option.map(param[1], v => [
634
+ key,
635
+ v
636
+ ]);
637
+ });
610
638
  let metadata = Object.fromEntries([
611
639
  [
612
640
  "environment",
613
- Pulumi.getStack()
641
+ Stdlib_Option.getOr(provenance.environment, stack)
642
+ ],
643
+ [
644
+ "stack",
645
+ stack
614
646
  ],
615
647
  [
616
648
  "region",
@@ -622,13 +654,17 @@ function exportDeploymentMetadata() {
622
654
  ],
623
655
  [
624
656
  "gitSha",
625
- Stdlib_Option.getOr(process.env["GITHUB_SHA"], "unknown")
657
+ Stdlib_Option.getOr(provenance.commit, "unknown")
658
+ ],
659
+ [
660
+ "dirty",
661
+ provenance.dirty ? "true" : "false"
626
662
  ],
627
663
  [
628
664
  "actor",
629
665
  Stdlib_Option.getOr(process.env["GITHUB_ACTOR"], "unknown")
630
666
  ]
631
- ]);
667
+ ].concat(optional));
632
668
  Pulumi$Pulumi.$$export("deploymentMetadata", Pulumi.output(Stdlib_Dict.mapValues(metadata, prim => prim)));
633
669
  }
634
670
 
@@ -725,6 +761,7 @@ function exportPluginOutputs(pluginOutputs) {
725
761
  }
726
762
  let actor = Stdlib_Option.getOr(Stdlib_Option.orElse(Stdlib_Option.orElse(process.env["GITHUB_ACTOR"], process.env["CI_COMMIT_AUTHOR"]), process.env["USER"]), "local");
727
763
  let deploymentId = Stdlib_Option.getOr(Stdlib_Option.orElse(process.env["GITHUB_SHA"], process.env["CI_COMMIT_SHA"]), new Date().toISOString());
764
+ let provenance = DeploymentProvenance$Reventless.current();
728
765
  let resolveAggregates = Output$Pulumi.flatMap(pluginOutputs.aggregates, aggs => Pulumi.all(Object.entries(aggs).map(param => {
729
766
  let name = param[0];
730
767
  return Aggregate$ReventlessCore.toResolvedOutputs(param[1]).apply(resolved => ({
@@ -904,11 +941,16 @@ function exportPluginOutputs(pluginOutputs) {
904
941
  return hook({
905
942
  name: name,
906
943
  version: version,
907
- environment: Pulumi.getStack(),
944
+ environment: Stdlib_Option.getOr(provenance.environment, Pulumi.getStack()),
908
945
  stackName: Pulumi.getStack(),
909
946
  deployedAt: new Date().toISOString(),
910
947
  actor: actor,
911
948
  deploymentId: deploymentId,
949
+ commit: provenance.commit,
950
+ dirty: provenance.dirty ? true : undefined,
951
+ purpose: provenance.purpose,
952
+ tag: provenance.tag,
953
+ pullRequest: provenance.pullRequest,
912
954
  kind: Stdlib_Option.flatMap(meta, m => m.kind),
913
955
  displayName: Stdlib_Option.flatMap(meta, m => m.displayName),
914
956
  vendor: Stdlib_Option.flatMap(meta, m => m.vendor),
@@ -793,15 +793,32 @@ let commandLevelAndId = (
793
793
  }
794
794
 
795
795
  // The server's rule as keys a client checks against `identity.groups ++
796
- // config.accessTiers`; an any-of, since `isAllowed` is `some`. The rules asking
797
- // for nothing checkable — and `DenyAll` — publish no keys rather than an
796
+ // config.accessTiers`; an any-of, since `isAllowed` is `some`. Mapped groups, not
797
+ // role names: a key no token carries would gate the surface shut. The rules
798
+ // asking for nothing checkable — and `DenyAll` — publish no keys rather than an
798
799
  // unsatisfiable one.
799
800
  let accessKeysFor = (rule: Reventless.Authorization.permission): option<array<string>> =>
800
- switch rule {
801
- | AllowGroups(groups) if groups->Array.length > 0 => Some(groups)
802
- | AllowGroups(_) | AllowAuthenticated | AllowAnonymous | DenyAll => None
801
+ switch Reventless.Authorization.groupsOf(rule) {
802
+ | [] => None
803
+ | groups => Some(groups)
803
804
  }
804
805
 
806
+ let nonEmpty = (xs: array<string>): option<array<string>> => xs->Array.length > 0 ? Some(xs) : None
807
+
808
+ // The same rule's roles before mapping, which the deploy check compares with what
809
+ // the platform provides.
810
+ let rolesFor = (rule: Reventless.Authorization.permission): option<array<string>> =>
811
+ rule->Reventless.Authorization.rolesOf->Array.map(role => (role :> string))->nonEmpty
812
+
813
+ // A stand-in for one constructor, the same shape the resolver builds at call time:
814
+ // a payload-bearing variant compiles to `{TAG, ...}`, a payload-less one to a bare
815
+ // string. Enough for the per-command switches the PPX generates, which match on
816
+ // the constructor alone.
817
+ let syntheticCommand = (~schema: S.t<unknown>, variantName: string): 'command =>
818
+ Reventless.DcbTag.isVariantPayloadBearing(schema, variantName)
819
+ ? {"TAG": variantName}->Obj.magic
820
+ : variantName->Obj.magic
821
+
805
822
  // Each mutation argument's GraphQL type onto its property, so a consumer declares
806
823
  // the variable the server expects. Mutates the freshly derived schema in place.
807
824
  let annotateArgTypes = (schema: JSON.t, argTypes: dict<string>): JSON.t => {
@@ -860,15 +877,8 @@ let toCommandDef = (
860
877
  ~variantName,
861
878
  properties,
862
879
  )
863
- // Evaluated against a synthetic value per constructor, the same shape the
864
- // resolver builds at call time: a payload-bearing variant compiles to
865
- // `{TAG, ...}`, a payload-less one to a bare string.
866
- let syntheticCommand: unknown = Reventless.DcbTag.isVariantPayloadBearing(
867
- parentSchema,
868
- variantName,
869
- )
870
- ? {"TAG": variantName}->Obj.magic
871
- : variantName->Obj.magic
880
+ // Evaluated against a synthetic value per constructor; see `syntheticCommand`.
881
+ let syntheticCommand: unknown = syntheticCommand(~schema=parentSchema, variantName)
872
882
  // The spec's own switch, which is exhaustive — so it also speaks for a
873
883
  // constructor the host did not declare but spliced from a trait.
874
884
  //
@@ -937,7 +947,8 @@ let toCommandDef = (
937
947
  | Some(excluded) => !(excluded->Set.has(variantName))
938
948
  | None => true
939
949
  }
940
- let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand))
950
+ let rule = commandAuthorization(syntheticCommand)
951
+ let requiredAccess = accessKeysFor(rule)
941
952
  // See the note on the record's `mutationField` for why a non-exposed
942
953
  // variant gets the empty sentinel. It has no callable field, and the
943
954
  // argument type names are composed *from* that field name, so there is
@@ -975,6 +986,7 @@ let toCommandDef = (
975
986
  ?allowedStatesSource,
976
987
  apiExposed: Some(apiExposed),
977
988
  requiredAccess,
989
+ requiredRoles: ?rolesFor(rule),
978
990
  // Resolved from this constructor's own properties, not the union's: two
979
991
  // commands in one slice can disagree about whether they record an owner,
980
992
  // and the write path stamps per constructor for the same reason.
@@ -1094,6 +1106,7 @@ let queryableDefFromSpec = (
1094
1106
  idField: keyField->Option.map(((f, _)) => f),
1095
1107
  idFieldSource: keyField->Option.map(((_, rung)) => rung),
1096
1108
  requiredAccess: accessKeysFor(authorization),
1109
+ requiredRoles: ?rolesFor(authorization),
1097
1110
  }
1098
1111
  }
1099
1112
 
@@ -1550,7 +1563,8 @@ let make = (
1550
1563
  singleQueryField: Some(qf.singleFieldName),
1551
1564
  idField: keyField->Option.map(((f, _)) => f),
1552
1565
  idFieldSource: keyField->Option.map(((_, rung)) => rung),
1553
- requiredAccess: accessKeysFor(R.Spec.authorization),
1566
+ requiredAccess: accessKeysFor(R.Spec.authorization->Reventless.Authorization.named),
1567
+ requiredRoles: ?rolesFor(R.Spec.authorization->Reventless.Authorization.named),
1554
1568
  }: Reventless.Plugin.queryableDef
1555
1569
  )
1556
1570
  })
@@ -1590,7 +1604,8 @@ let make = (
1590
1604
  singleQueryField: Some(qf.singleFieldName),
1591
1605
  idField: keyField->Option.map(((f, _)) => f),
1592
1606
  idFieldSource: keyField->Option.map(((_, rung)) => rung),
1593
- requiredAccess: accessKeysFor(SVS.Spec.authorization),
1607
+ requiredAccess: accessKeysFor(SVS.Spec.authorization->Reventless.Authorization.named),
1608
+ requiredRoles: ?rolesFor(SVS.Spec.authorization->Reventless.Authorization.named),
1594
1609
  }: Reventless.Plugin.queryableDef
1595
1610
  )
1596
1611
  })
@@ -1690,7 +1705,8 @@ let make = (
1690
1705
  ~commandSchema=SCS.Spec.commandSchema->S.castToUnknown,
1691
1706
  ~variant=variantName,
1692
1707
  ),
1693
- ~commandAuthorization=SCS.Spec.commandAuthorization->Obj.magic,
1708
+ ~commandAuthorization=command =>
1709
+ SCS.Spec.commandAuthorization(command->Obj.magic)->Reventless.Authorization.named,
1694
1710
  ~commandTransition=SCS.Spec.commandTransition->Obj.magic,
1695
1711
  ~derivedEdgeFor=derivedEdgeFor(~component=SCS.Spec.name, ...),
1696
1712
  SCS.Spec.commandSchema->S.castToUnknown,
@@ -1724,7 +1740,8 @@ let make = (
1724
1740
  ~aggregate=A.Spec.name,
1725
1741
  ~command=variantName,
1726
1742
  ),
1727
- ~commandAuthorization=A.Spec.commandAuthorization->Obj.magic,
1743
+ ~commandAuthorization=command =>
1744
+ A.Spec.commandAuthorization(command->Obj.magic)->Reventless.Authorization.named,
1728
1745
  ~commandTransition=A.Spec.commandTransition->Obj.magic,
1729
1746
  ~derivedEdgeFor=derivedEdgeFor(~component=A.Spec.name, ...),
1730
1747
  A.Spec.commandSchema->S.castToUnknown,
@@ -1798,6 +1815,22 @@ let make = (
1798
1815
  targetName: ITS.Spec.targetName,
1799
1816
  externalSystem: ITS.Spec.externalSystem,
1800
1817
  chapter: chapterOf(ITS.Spec.name),
1818
+ // Per command, as the resolver evaluates the rule, then joined: the slice
1819
+ // publishes no command defs to carry them.
1820
+ requiredRoles: ?(
1821
+ Reventless.DcbTag.extractAllVariantNames(ITS.Spec.commandSchema)
1822
+ ->Array.flatMap(variantName =>
1823
+ syntheticCommand(~schema=ITS.Spec.commandSchema->S.castToUnknown, variantName)
1824
+ ->ITS.Spec.commandAuthorization
1825
+ ->Reventless.Authorization.named
1826
+ ->Reventless.Authorization.rolesOf
1827
+ ->Array.map(role => (role :> string))
1828
+ )
1829
+ ->Set.fromArray
1830
+ ->Set.values
1831
+ ->Array.fromIterator
1832
+ ->nonEmpty
1833
+ ),
1801
1834
  })
1802
1835
 
1803
1836
  // ── Extensions ───────────────────────────────────────────────────────────
@@ -21,6 +21,7 @@ import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
21
21
  import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
22
22
  import * as Transition$Reventless from "@reventlessdev/reventless-spec/src/types/Transition.res.mjs";
23
23
  import * as DisplayName$Reventless from "@reventlessdev/reventless-spec/src/components/DisplayName.res.mjs";
24
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
24
25
  import * as Api_Naming$ReventlessCore from "../../components/Api/Api_Naming.res.mjs";
25
26
  import * as CapabilityNeed$Reventless from "@reventlessdev/reventless-spec/src/semantic/CapabilityNeed.res.mjs";
26
27
  import * as SchemaType$ReventlessCore from "../../components/Api/SchemaType.res.mjs";
@@ -617,15 +618,32 @@ function commandLevelAndId(isAggregate, partitionKey, variantName, properties) {
617
618
  }
618
619
 
619
620
  function accessKeysFor(rule) {
620
- if (typeof rule !== "object") {
621
- return;
622
- }
623
- let groups = rule._0;
621
+ let groups = Authorization$Reventless.groupsOf(rule);
624
622
  if (groups.length !== 0) {
625
623
  return groups;
626
624
  }
627
625
  }
628
626
 
627
+ function nonEmpty(xs) {
628
+ if (xs.length !== 0) {
629
+ return xs;
630
+ }
631
+ }
632
+
633
+ function rolesFor(rule) {
634
+ return nonEmpty(Authorization$Reventless.rolesOf(rule).map(role => role));
635
+ }
636
+
637
+ function syntheticCommand(schema, variantName) {
638
+ if (DcbTag$Reventless.isVariantPayloadBearing(schema, variantName)) {
639
+ return {
640
+ TAG: variantName
641
+ };
642
+ } else {
643
+ return variantName;
644
+ }
645
+ }
646
+
629
647
  function annotateArgTypes(schema, argTypes) {
630
648
  Stdlib_Option.forEach(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(schema), o => o["properties"]), Stdlib_JSON.Decode.object), props => {
631
649
  Object.entries(props).forEach(param => {
@@ -644,10 +662,8 @@ function toCommandDef(identityViews, isAggregate, partitionKey, mutationFieldFor
644
662
  let mkDef = (variantName, properties) => {
645
663
  let match = commandLevelAndId(isAggregate, partitionKey, variantName, properties);
646
664
  let aggregateIdField = match[1];
647
- let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
648
- TAG: variantName
649
- }) : variantName;
650
- let declared = commandTransition(syntheticCommand);
665
+ let syntheticCommand$1 = syntheticCommand(parentSchema, variantName);
666
+ let declared = commandTransition(syntheticCommand$1);
651
667
  let derived = derivedEdgeFor(variantName);
652
668
  let unrestricted = Transition$Reventless.isUnrestricted(declared);
653
669
  let match$1 = unrestricted ? undefined : Stdlib_Option.flatMap(derived, d => {
@@ -684,7 +700,8 @@ function toCommandDef(identityViews, isAggregate, partitionKey, mutationFieldFor
684
700
  let excluded = ApiNoApiHelpers$ReventlessCore.getExcludedVariants(parentSchema);
685
701
  apiExposed = excluded !== undefined ? !excluded.has(variantName) : true;
686
702
  }
687
- let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand));
703
+ let rule = commandAuthorization(syntheticCommand$1);
704
+ let requiredAccess = accessKeysFor(rule);
688
705
  let mutationField = apiExposed ? mutationFieldFor(variantName) : "";
689
706
  let jsonSchema = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(apiExposed, mutationField, v);
690
707
  let annotatedSchema = apiExposed ? Stdlib_Option.mapOr(GraphQL_FragmentGenerator$ReventlessCore.mutationArgTypes(mutationField, v), jsonSchema, __x => annotateArgTypes(jsonSchema, __x)) : jsonSchema;
@@ -700,6 +717,7 @@ function toCommandDef(identityViews, isAggregate, partitionKey, mutationFieldFor
700
717
  allowedStatesSource: match$3[1],
701
718
  apiExposed: apiExposed,
702
719
  requiredAccess: requiredAccess,
720
+ requiredRoles: rolesFor(rule),
703
721
  ownerField: Owner$Reventless.fieldNamesOfProperties(properties)[0]
704
722
  };
705
723
  };
@@ -770,7 +788,8 @@ function queryableDefFromSpec(plugin, name, stateSchema, authorization, visibili
770
788
  singleQueryField: qf.singleFieldName,
771
789
  idField: Stdlib_Option.map(keyField, param => param[0]),
772
790
  idFieldSource: Stdlib_Option.map(keyField, param => param[1]),
773
- requiredAccess: accessKeysFor(authorization)
791
+ requiredAccess: accessKeysFor(authorization),
792
+ requiredRoles: rolesFor(authorization)
774
793
  };
775
794
  }
776
795
 
@@ -1080,7 +1099,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1080
1099
  singleQueryField: qf.singleFieldName,
1081
1100
  idField: Stdlib_Option.map(keyField, param => param[0]),
1082
1101
  idFieldSource: Stdlib_Option.map(keyField, param => param[1]),
1083
- requiredAccess: accessKeysFor(R.Spec.authorization)
1102
+ requiredAccess: accessKeysFor(Authorization$Reventless.named(R.Spec.authorization)),
1103
+ requiredRoles: rolesFor(Authorization$Reventless.named(R.Spec.authorization))
1084
1104
  };
1085
1105
  });
1086
1106
  let stateViewDefs = stateViewSlices.map((SVS, i) => {
@@ -1112,7 +1132,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1112
1132
  singleQueryField: qf.singleFieldName,
1113
1133
  idField: Stdlib_Option.map(keyField, param => param[0]),
1114
1134
  idFieldSource: Stdlib_Option.map(keyField, param => param[1]),
1115
- requiredAccess: accessKeysFor(SVS.Spec.authorization)
1135
+ requiredAccess: accessKeysFor(Authorization$Reventless.named(SVS.Spec.authorization)),
1136
+ requiredRoles: rolesFor(Authorization$Reventless.named(SVS.Spec.authorization))
1116
1137
  };
1117
1138
  });
1118
1139
  let dcbSliceSchemas = stateChangeSlices.map(SCS => ({
@@ -1197,7 +1218,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1197
1218
  let consumed = match$1[1];
1198
1219
  return {
1199
1220
  name: SCS.Spec.name,
1200
- commands: extractCommandDefs(identityViews, false, partitionBySlice[SCS.Spec.name], variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandTransition, extra => derivedEdgeFor(SCS.Spec.name, extra), SCS.Spec.commandSchema),
1221
+ commands: extractCommandDefs(identityViews, false, partitionBySlice[SCS.Spec.name], variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), command => Authorization$Reventless.named(SCS.Spec.commandAuthorization(command)), SCS.Spec.commandTransition, extra => derivedEdgeFor(SCS.Spec.name, extra), SCS.Spec.commandSchema),
1201
1222
  producedEventTypes: produced,
1202
1223
  consumedEventTypes: consumed,
1203
1224
  linkedViews: linkedSvsFor(produced),
@@ -1212,7 +1233,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1212
1233
  let produced = match[1];
1213
1234
  return {
1214
1235
  name: A.Spec.name,
1215
- commands: extractCommandDefs(identityViews, true, undefined, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, extra => derivedEdgeFor(A.Spec.name, extra), A.Spec.commandSchema),
1236
+ commands: extractCommandDefs(identityViews, true, undefined, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), command => Authorization$Reventless.named(A.Spec.commandAuthorization(command)), A.Spec.commandTransition, extra => derivedEdgeFor(A.Spec.name, extra), A.Spec.commandSchema),
1216
1237
  producedEventTypes: produced,
1217
1238
  consumedEventTypes: [],
1218
1239
  linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
@@ -1246,7 +1267,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1246
1267
  commandTypes: qualify(name, DcbTag$Reventless.extractAllVariantNames(ITS.Spec.commandSchema)),
1247
1268
  targetName: ITS.Spec.targetName,
1248
1269
  externalSystem: ITS.Spec.externalSystem,
1249
- chapter: componentChapters[ITS.Spec.name]
1270
+ chapter: componentChapters[ITS.Spec.name],
1271
+ requiredRoles: nonEmpty(Array.from(new Set(DcbTag$Reventless.extractAllVariantNames(ITS.Spec.commandSchema).flatMap(variantName => Authorization$Reventless.rolesOf(Authorization$Reventless.named(ITS.Spec.commandAuthorization(syntheticCommand(ITS.Spec.commandSchema, variantName)))).map(role => role))).values()))
1250
1272
  }));
1251
1273
  let tableFailures = [];
1252
1274
  let tableWarnings = [];
@@ -1549,6 +1571,9 @@ export {
1549
1571
  isCreateCommandName,
1550
1572
  commandLevelAndId,
1551
1573
  accessKeysFor,
1574
+ nonEmpty,
1575
+ rolesFor,
1576
+ syntheticCommand,
1552
1577
  annotateArgTypes,
1553
1578
  toCommandDef,
1554
1579
  extractCommandDefs,
@@ -10,7 +10,7 @@
10
10
  // this (spec-level `permission` outranks the legacy `{tableName, group}` pair),
11
11
  // and so does the `requiredAccess` the platform publishes — so a shell can gate
12
12
  // the Plugins view up front instead of offering it and meeting a refusal.
13
- @@reventless.authorize(AllowGroups(["Admin"]))
13
+ @@reventless.authorize(AllowRoles([Admin]))
14
14
 
15
15
  // Current view — one row per plugin **name**, holding the currently-active
16
16
  // version's definition. Keyed by the aggregate id (= plugin name). The status
@@ -64,7 +64,7 @@ let stateSchema$1 = Sury.$Metadata_set(stateSchema, StateAnnotations$Reventless.
64
64
  });
65
65
 
66
66
  let authorization = {
67
- TAG: "AllowGroups",
67
+ TAG: "AllowRoles",
68
68
  _0: ["Admin"]
69
69
  };
70
70
 
@@ -254,7 +254,9 @@ let guards: array<guard> = [
254
254
  recordFields: schemaKeys(commandDefSchema),
255
255
  wireFields: jsonKeys(Platform_ComponentDefinitionsApi.encodeCommandDef(cmd)),
256
256
  wireOnly: [],
257
- recordOnly: [],
257
+ // The roles behind `requiredAccess`, read by the deploy check in-process.
258
+ // A client gates on the mapped groups and has no use for role names.
259
+ recordOnly: ["requiredRoles"],
258
260
  },
259
261
  {
260
262
  gqlType: "Platform_EventDef",
@@ -286,7 +288,9 @@ let guards: array<guard> = [
286
288
  recordFields: schemaKeys(queryableDefSchema),
287
289
  wireFields: jsonKeys(Platform_ComponentDefinitionsApi.encodeQueryableDef(queryable)),
288
290
  wireOnly: [],
289
- recordOnly: [],
291
+ // The roles behind `requiredAccess`, read by the deploy check in-process.
292
+ // A client gates on the mapped groups and has no use for role names.
293
+ recordOnly: ["requiredRoles"],
290
294
  },
291
295
  {
292
296
  gqlType: "Platform_AutomationSliceDef",
@@ -314,7 +318,9 @@ let guards: array<guard> = [
314
318
  Platform_ComponentDefinitionsApi.encodeInboundTranslationSliceDef(inbound),
315
319
  ),
316
320
  wireOnly: [],
317
- recordOnly: [],
321
+ // The roles behind `requiredAccess`, read by the deploy check in-process.
322
+ // A client gates on the mapped groups and has no use for role names.
323
+ recordOnly: ["requiredRoles"],
318
324
  },
319
325
  {
320
326
  gqlType: "Platform_ExtensionDef",
@@ -390,7 +390,7 @@ let guards = [
390
390
  recordFields: schemaKeys(Plugin$Reventless.commandDefSchema),
391
391
  wireFields: jsonKeys(Platform_ComponentDefinitionsApi$ReventlessCore.encodeCommandDef(cmd)),
392
392
  wireOnly: [],
393
- recordOnly: []
393
+ recordOnly: ["requiredRoles"]
394
394
  },
395
395
  {
396
396
  gqlType: "Platform_EventDef",
@@ -422,7 +422,7 @@ let guards = [
422
422
  recordFields: schemaKeys(Plugin$Reventless.queryableDefSchema),
423
423
  wireFields: jsonKeys(Platform_ComponentDefinitionsApi$ReventlessCore.encodeQueryableDef(queryable)),
424
424
  wireOnly: [],
425
- recordOnly: []
425
+ recordOnly: ["requiredRoles"]
426
426
  },
427
427
  {
428
428
  gqlType: "Platform_AutomationSliceDef",
@@ -446,7 +446,7 @@ let guards = [
446
446
  recordFields: schemaKeys(Plugin$Reventless.inboundTranslationSliceDefSchema),
447
447
  wireFields: jsonKeys(Platform_ComponentDefinitionsApi$ReventlessCore.encodeInboundTranslationSliceDef(inbound)),
448
448
  wireOnly: [],
449
- recordOnly: []
449
+ recordOnly: ["requiredRoles"]
450
450
  },
451
451
  {
452
452
  gqlType: "Platform_ExtensionDef",