@reventlessdev/reventless-core 3.0.0-alpha.285 → 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 (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +6 -6
  3. package/src/Roles.res +3 -0
  4. package/src/Roles.res.mjs +2 -0
  5. package/src/admin/Platform_AdminGroup.res +12 -0
  6. package/src/admin/Platform_AdminGroup.res.mjs +23 -0
  7. package/src/admin/Platform_Admin_Structure.res +14 -10
  8. package/src/admin/Platform_Admin_Structure.res.mjs +22 -33
  9. package/src/components/AutomationSlice/AutomationSlice_Builder.res +2 -1
  10. package/src/components/Counter/Counter_Builder.res +4 -2
  11. package/src/components/Dcb/Dcb_Builder.res +9 -6
  12. package/src/components/Dcb/Dcb_Builder.res.mjs +6 -6
  13. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +2 -1
  14. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +2 -1
  15. package/src/components/QueryDb/QueryDb_Builder.res +1 -1
  16. package/src/components/QueryDb/QueryDb_Builder.res.mjs +3 -2
  17. package/src/components/StateViewSlice/StateViewSlice_Builder.res +1 -0
  18. package/src/plugin/api/PluginBaseFragment.res +1 -1
  19. package/src/plugin/api/PluginBaseFragment.res.mjs +2 -1
  20. package/src/plugin/component/Plugin_Builder.res +7 -3
  21. package/src/plugin/component/Plugin_Builder.res.mjs +6 -3
  22. package/src/plugin/component/Plugin_Helpers.res +2 -1
  23. package/src/plugin/component/Plugin_Helpers.res.mjs +2 -1
  24. package/src/plugin/component/Plugin_Structure.res +52 -19
  25. package/src/plugin/component/Plugin_Structure.res.mjs +40 -15
  26. package/src/plugin/lifecycle/PluginsReadModelSpec.res +1 -1
  27. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +1 -1
  28. package/tests/admin/AdminApiSchemaDriftTest.res +9 -3
  29. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +3 -3
  30. package/tests/admin/Platform_Admin_StructureTest.res +17 -14
  31. package/tests/admin/Platform_Admin_StructureTest.res.mjs +31 -19
  32. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +8 -72
  33. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +9 -39
  34. package/tests/api/GraphQL_FragmentGeneratorTest.res +1 -1
  35. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +1 -1
  36. package/tests/plugin/PluginStructureAccessTest.res +30 -1
  37. package/tests/plugin/PluginStructureAccessTest.res.mjs +28 -1
  38. package/tests/plugin/StateChange/PsGatedCommands.res +9 -1
  39. package/tests/plugin/StateChange/PsGatedCommands.res.mjs +4 -1
  40. package/tests/plugin/StateView/PsGatedView.res +1 -1
  41. package/tests/plugin/StateView/PsGatedView.res.mjs +1 -1
  42. package/tests/plugin/pluginDefinitionRequiredScalars.txt +5 -0
@@ -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",
@@ -27,7 +27,7 @@ let commandNamed = (name: string): commandDef =>
27
27
  let activate = () => commandNamed("Activate")
28
28
 
29
29
  describe("the Plugins read model declares its lifecycle", () => {
30
- let rm: queryableDef = Platform_Admin_Structure.pluginReadModel
30
+ let rm: queryableDef = Platform_Admin_Structure.pluginReadModel()
31
31
 
32
32
  testSync("names the field commands branch on", () =>
33
33
  expect(rm.lifecycleField)->toEqual(Some("status"))
@@ -206,25 +206,28 @@ describe("the internal commands declare the edges they move rows along", () => {
206
206
  // was open to everyone and the server refused anyone outside `Admin`. The
207
207
  // mismatch is invisible until a non-admin clicks the page.
208
208
  describe("the Plugins view publishes the rule the server enforces", () => {
209
- // 🚨 **This is also the only check `Reventless.AdminGroup.name` can carry.** A
210
- // PPX annotation takes a literal, so `@@reventless.authorize(AllowGroups(
211
- // ["Admin"]))` above cannot reference the constant — the constant's job is to
212
- // make everything *else* agree with the annotation, and nothing in the compiler
213
- // spans that gap. The actual side here comes from the expanded annotation and
214
- // the expected side from the constant, so the two are compared rather than
215
- // restated. Writing the literal on both sides would pass while they diverged.
216
- testSync("the spec declares the group, and it is the one the constant names", () =>
209
+ // The annotation names the role, so a renamed administrator group follows it
210
+ // without anyone editing the spec.
211
+ testSync("the spec declares the administrator role", () =>
217
212
  expect(PluginsReadModelSpec.authorization)->toEqual(
218
- Reventless.Authorization.AllowGroups([Reventless.AdminGroup.name]),
213
+ Reventless.Authorization.AllowRoles([Reventless.Role.admin]),
219
214
  )
220
215
  )
221
216
 
222
- testSync("and the published access keys are derived from it", () =>
223
- expect(Platform_Admin_Structure.pluginReadModel.requiredAccess)->toEqual(
224
- Some([Reventless.AdminGroup.name]),
217
+ testSync("and the published access keys are the group it maps to", () =>
218
+ expect(Platform_Admin_Structure.pluginReadModel().requiredAccess)->toEqual(
219
+ Some([Reventless.Role.adminGroup()]),
225
220
  )
226
221
  )
227
222
 
223
+ testSync("a renamed administrator group reaches the access keys", () => {
224
+ Reventless.Role.clearGroups()
225
+ Reventless.Role.setGroups([(Reventless.Role.admin, "ops")])
226
+ let keys = Platform_Admin_Structure.pluginReadModel().requiredAccess
227
+ Reventless.Role.clearGroups()
228
+ expect(keys)->toEqual(Some(["ops"]))
229
+ })
230
+
228
231
  // The API entry reads the same binding rather than restating it, so the two
229
232
  // cannot be edited apart.
230
233
  testSync("the query entry carries the spec's rule and no hand-written pair", () => {
@@ -243,7 +246,7 @@ describe("the Plugins view publishes the rule the server enforces", () => {
243
246
  // looked special. Asserted rather than assumed — the whole case for deriving
244
247
  // rests on the generic helpers reaching the same answers.
245
248
  describe("the Plugins queryableDef is what the generic helpers produce", () => {
246
- let rm: queryableDef = Platform_Admin_Structure.pluginReadModel
249
+ let rm: queryableDef = Platform_Admin_Structure.pluginReadModel()
247
250
 
248
251
  testSync("the field names are the generic ones", () =>
249
252
  expect((rm.queryField, rm.singleQueryField))->toEqual((
@@ -2,8 +2,8 @@
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 Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
5
6
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
- import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
7
7
  import * as Plugin_Structure$ReventlessCore from "../../src/plugin/component/Plugin_Structure.res.mjs";
8
8
  import * as PluginBaseFragment$ReventlessCore from "../../src/plugin/api/PluginBaseFragment.res.mjs";
9
9
  import * as PluginsReadModelSpec$ReventlessCore from "../../src/plugin/lifecycle/PluginsReadModelSpec.res.mjs";
@@ -19,19 +19,20 @@ function activate() {
19
19
  }
20
20
 
21
21
  globalThis.describe("the Plugins read model declares its lifecycle", () => {
22
+ let rm = Platform_Admin_Structure$ReventlessCore.pluginReadModel();
22
23
  globalThis.test("names the field commands branch on", () => {
23
- globalThis.expect(Platform_Admin_Structure$ReventlessCore.pluginReadModel.lifecycleField).toEqual("status");
24
+ globalThis.expect(rm.lifecycleField).toEqual("status");
24
25
  });
25
26
  globalThis.test("and the published wire name is unchanged by the rename", () => {
26
- let sdlDeclaresStatus = Stdlib_Option.mapOr(Platform_Admin_Structure$ReventlessCore.pluginReadModel.lifecycleField, false, f => f === "status") && Platform_Admin_Structure$ReventlessCore.pluginReadModel.queryField.startsWith("Platform_");
27
+ let sdlDeclaresStatus = Stdlib_Option.mapOr(rm.lifecycleField, false, f => f === "status") && rm.queryField.startsWith("Platform_");
27
28
  globalThis.expect(sdlDeclaresStatus).toEqual(true);
28
29
  });
29
30
  globalThis.test("is served on the encoded def under the new key", () => {
30
- let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodeQueryableDef(Platform_Admin_Structure$ReventlessCore.pluginReadModel));
31
+ let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodeQueryableDef(rm));
31
32
  globalThis.expect(json.includes(`"lifecycleField":"status"`)).toEqual(true);
32
33
  });
33
34
  globalThis.test("and never under the old one", () => {
34
- globalThis.expect(JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodeQueryableDef(Platform_Admin_Structure$ReventlessCore.pluginReadModel)).includes(`"statusField"`)).toEqual(false);
35
+ globalThis.expect(JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodeQueryableDef(rm)).includes(`"statusField"`)).toEqual(false);
35
36
  });
36
37
  });
37
38
 
@@ -155,14 +156,24 @@ globalThis.describe("the internal commands declare the edges they move rows alon
155
156
  });
156
157
 
157
158
  globalThis.describe("the Plugins view publishes the rule the server enforces", () => {
158
- globalThis.test("the spec declares the group, and it is the one the constant names", () => {
159
+ globalThis.test("the spec declares the administrator role", () => {
159
160
  globalThis.expect(PluginsReadModelSpec$ReventlessCore.authorization).toEqual({
160
- TAG: "AllowGroups",
161
- _0: [AdminGroup$Reventless.name]
161
+ TAG: "AllowRoles",
162
+ _0: [Role$Reventless.admin]
162
163
  });
163
164
  });
164
- globalThis.test("and the published access keys are derived from it", () => {
165
- globalThis.expect(Platform_Admin_Structure$ReventlessCore.pluginReadModel.requiredAccess).toEqual([AdminGroup$Reventless.name]);
165
+ globalThis.test("and the published access keys are the group it maps to", () => {
166
+ globalThis.expect(Platform_Admin_Structure$ReventlessCore.pluginReadModel().requiredAccess).toEqual([Role$Reventless.adminGroup()]);
167
+ });
168
+ globalThis.test("a renamed administrator group reaches the access keys", () => {
169
+ Role$Reventless.clearGroups();
170
+ Role$Reventless.setGroups([[
171
+ Role$Reventless.admin,
172
+ "ops"
173
+ ]]);
174
+ let keys = Platform_Admin_Structure$ReventlessCore.pluginReadModel().requiredAccess;
175
+ Role$Reventless.clearGroups();
176
+ globalThis.expect(keys).toEqual(["ops"]);
166
177
  });
167
178
  globalThis.test("the query entry carries the spec's rule and no hand-written pair", () => {
168
179
  let entry = PluginBaseFragment$ReventlessCore.queryEntries[0];
@@ -177,10 +188,11 @@ globalThis.describe("the Plugins view publishes the rule the server enforces", (
177
188
  });
178
189
 
179
190
  globalThis.describe("the Plugins queryableDef is what the generic helpers produce", () => {
191
+ let rm = Platform_Admin_Structure$ReventlessCore.pluginReadModel();
180
192
  globalThis.test("the field names are the generic ones", () => {
181
193
  globalThis.expect([
182
- Platform_Admin_Structure$ReventlessCore.pluginReadModel.queryField,
183
- Platform_Admin_Structure$ReventlessCore.pluginReadModel.singleQueryField
194
+ rm.queryField,
195
+ rm.singleQueryField
184
196
  ]).toEqual([
185
197
  "Platform_Plugins",
186
198
  "Platform_Plugin"
@@ -188,9 +200,9 @@ globalThis.describe("the Plugins queryableDef is what the generic helpers produc
188
200
  });
189
201
  globalThis.test("the label ladder reaches the same field, by convention", () => {
190
202
  globalThis.expect([
191
- Platform_Admin_Structure$ReventlessCore.pluginReadModel.labelField,
192
- Platform_Admin_Structure$ReventlessCore.pluginReadModel.labelFieldSource,
193
- Platform_Admin_Structure$ReventlessCore.pluginReadModel.searchableFields
203
+ rm.labelField,
204
+ rm.labelFieldSource,
205
+ rm.searchableFields
194
206
  ]).toEqual([
195
207
  "name",
196
208
  "convention",
@@ -199,15 +211,15 @@ globalThis.describe("the Plugins queryableDef is what the generic helpers produc
199
211
  });
200
212
  globalThis.test("the key ladder finds no id field, as the hand-written def claimed", () => {
201
213
  globalThis.expect([
202
- Platform_Admin_Structure$ReventlessCore.pluginReadModel.idField,
203
- Platform_Admin_Structure$ReventlessCore.pluginReadModel.idFieldSource
214
+ rm.idField,
215
+ rm.idFieldSource
204
216
  ]).toEqual([
205
217
  undefined,
206
218
  undefined
207
219
  ]);
208
220
  });
209
221
  globalThis.test("the blob fields are published, and marked hidden", () => {
210
- let props = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(Platform_Admin_Structure$ReventlessCore.pluginReadModel.schema)), o => o["properties"]), Stdlib_JSON.Decode.object), {});
222
+ let props = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(rm.schema)), o => o["properties"]), Stdlib_JSON.Decode.object), {});
211
223
  let isHidden = key => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(props[key], Stdlib_JSON.Decode.object), o => o["x-reventless-hidden"]), Stdlib_JSON.Decode.bool);
212
224
  globalThis.expect([
213
225
  isHidden("apiSchemaFragment"),
@@ -218,7 +230,7 @@ globalThis.describe("the Plugins queryableDef is what the generic helpers produc
218
230
  ]);
219
231
  });
220
232
  globalThis.test("while the internal fields are not published at all", () => {
221
- let props = Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(Platform_Admin_Structure$ReventlessCore.pluginReadModel.schema)), o => o["properties"]), Stdlib_JSON.Decode.object), [], prim => Object.keys(prim));
233
+ let props = Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(rm.schema)), o => o["properties"]), Stdlib_JSON.Decode.object), [], prim => Object.keys(prim));
222
234
  globalThis.expect([
223
235
  props.includes("eventCollector"),
224
236
  props.includes("extensionNames"),
@@ -311,40 +311,14 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
311
311
  idFieldSource: internalQbl_idFieldSource,
312
312
  requiredAccess: undefined
313
313
  };
314
- let internalSlice_consumedEventTypes = internalQbl_consumedEventTypes;
315
- let internalSlice_linkedWriteSide = internalQbl_linkedWriteSide;
316
- let internalSlice_searchableFields = internalQbl_searchableFields;
317
- let internalSlice_labelFieldSource = "convention";
318
- let internalSlice_visibility = "Internal";
319
- let internalSlice_singleQueryField = "Ordering_AvailableProduct";
320
- let internalSlice_idField = "productId";
321
- let internalSlice_idFieldSource = "sole";
322
- let internalSlice = {
323
- name: "PendingShipments",
324
- queryField: "Ordering_PendingShipments",
325
- schema: "{}",
326
- consumedEventTypes: internalSlice_consumedEventTypes,
327
- linkedWriteSide: internalSlice_linkedWriteSide,
328
- labelField: "name",
329
- searchableFields: internalSlice_searchableFields,
330
- labelFieldSource: internalSlice_labelFieldSource,
331
- lifecycleField: undefined,
332
- ownerField: undefined,
333
- retiredField: undefined,
334
- retiredValues: undefined,
335
- namedWhenRetired: undefined,
336
- visibility: internalSlice_visibility,
337
- chapter: undefined,
338
- singleQueryField: internalSlice_singleQueryField,
339
- idField: internalSlice_idField,
340
- idFieldSource: internalSlice_idFieldSource,
341
- requiredAccess: undefined
342
- };
314
+ let newrecord = {...internalQbl};
315
+ newrecord.queryField = "Ordering_PendingShipments";
316
+ newrecord.name = "PendingShipments";
343
317
  let mixed_readModels = [
344
318
  qbl,
345
319
  internalQbl
346
320
  ];
347
- let mixed_stateViewSlices = [internalSlice];
321
+ let mixed_stateViewSlices = [newrecord];
348
322
  let mixed_stateChangeSlices = structure_stateChangeSlices;
349
323
  let mixed_aggregates = structure_aggregates;
350
324
  let mixed_automationSlices = structure_automationSlices;
@@ -515,28 +489,9 @@ globalThis.describe("singleQueryField reaches the caller", () => {
515
489
  globalThis.expect(json.includes("\"singleQueryField\":\"Catalog_Product\"")).toEqual(true);
516
490
  });
517
491
  globalThis.test("encodes an unstated singular as null", () => {
492
+ let newrecord = {...qbl};
518
493
  let unstated = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", undefined, {
519
- readModels: [{
520
- name: "Products",
521
- queryField: "Catalog_Products",
522
- schema: "{}",
523
- consumedEventTypes: qbl_consumedEventTypes,
524
- linkedWriteSide: qbl_linkedWriteSide,
525
- labelField: "displayName",
526
- searchableFields: qbl_searchableFields,
527
- labelFieldSource: "annotation",
528
- lifecycleField: undefined,
529
- ownerField: undefined,
530
- retiredField: undefined,
531
- retiredValues: undefined,
532
- namedWhenRetired: undefined,
533
- visibility: undefined,
534
- chapter: undefined,
535
- singleQueryField: undefined,
536
- idField: "productId",
537
- idFieldSource: "convention",
538
- requiredAccess: undefined
539
- }],
494
+ readModels: [(newrecord.singleQueryField = undefined, newrecord)],
540
495
  stateViewSlices: structure_stateViewSlices,
541
496
  stateChangeSlices: structure_stateChangeSlices,
542
497
  aggregates: structure_aggregates,
@@ -565,28 +520,9 @@ globalThis.describe("idField / idFieldSource reach the caller", () => {
565
520
  globalThis.expect(json.includes("\"idFieldSource\":\"convention\"")).toEqual(true);
566
521
  });
567
522
  globalThis.test("encodes an unresolved key as null on both fields", () => {
523
+ let newrecord = {...qbl};
568
524
  let unresolved = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", undefined, {
569
- readModels: [{
570
- name: "Products",
571
- queryField: "Catalog_Products",
572
- schema: "{}",
573
- consumedEventTypes: qbl_consumedEventTypes,
574
- linkedWriteSide: qbl_linkedWriteSide,
575
- labelField: "displayName",
576
- searchableFields: qbl_searchableFields,
577
- labelFieldSource: "annotation",
578
- lifecycleField: undefined,
579
- ownerField: undefined,
580
- retiredField: undefined,
581
- retiredValues: undefined,
582
- namedWhenRetired: undefined,
583
- visibility: undefined,
584
- chapter: undefined,
585
- singleQueryField: "Catalog_Product",
586
- idField: undefined,
587
- idFieldSource: undefined,
588
- requiredAccess: undefined
589
- }],
525
+ readModels: [(newrecord.idFieldSource = undefined, newrecord.idField = undefined, newrecord)],
590
526
  stateViewSlices: structure_stateViewSlices,
591
527
  stateChangeSlices: structure_stateChangeSlices,
592
528
  aggregates: structure_aggregates,