@reventlessdev/reventless-core 3.0.0-alpha.223 → 3.0.0-alpha.225

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 (51) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +6 -6
  3. package/src/admin/Platform_Admin_Structure.res +16 -0
  4. package/src/admin/Platform_Admin_Structure.res.mjs +14 -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 +19 -2
  8. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +30 -2
  9. package/src/components/Api/Api_Ids.res +63 -0
  10. package/src/components/Api/Api_Ids.res.mjs +43 -0
  11. package/src/components/Api/GraphQL_FragmentGenerator.res +121 -28
  12. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +97 -27
  13. package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
  14. package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
  15. package/src/components/Api/QueryDbListQuery.res +38 -6
  16. package/src/components/Api/QueryDbListQuery.res.mjs +14 -5
  17. package/src/components/Api/SchemaType.res +36 -2
  18. package/src/components/Api/SchemaType.res.mjs +31 -10
  19. package/src/components/Api/SuryToJsonSchema.res +19 -0
  20. package/src/components/Api/SuryToJsonSchema.res.mjs +18 -4
  21. package/src/components/CommandGenerator/CommandGenerator_Callback.res +48 -0
  22. package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +27 -1
  23. package/src/plugin/component/Plugin_Structure.res +78 -3
  24. package/src/plugin/component/Plugin_Structure.res.mjs +38 -9
  25. package/tests/admin/Platform_BakedManifestTest.res +244 -0
  26. package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
  27. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +73 -0
  28. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +135 -6
  29. package/tests/admin/Platform_PluginStructuresApiTest.res +12 -0
  30. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +27 -2
  31. package/tests/api/Api_IdsTest.res +143 -0
  32. package/tests/api/Api_IdsTest.res.mjs +97 -0
  33. package/tests/api/GraphQL_FragmentGeneratorTest.res +169 -0
  34. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +179 -0
  35. package/tests/api/OwnerScopeDiagnosticsTest.res +109 -0
  36. package/tests/api/OwnerScopeDiagnosticsTest.res.mjs +91 -0
  37. package/tests/api/SuryToJsonSchemaTest.res +45 -0
  38. package/tests/api/SuryToJsonSchemaTest.res.mjs +23 -0
  39. package/tests/commandgenerator/OwnerStampingTest.res +174 -0
  40. package/tests/commandgenerator/OwnerStampingTest.res.mjs +195 -0
  41. package/tests/plugin/PluginStructureAccessTest.res +110 -0
  42. package/tests/plugin/PluginStructureAccessTest.res.mjs +144 -0
  43. package/tests/plugin/PluginStructureTest.res +90 -0
  44. package/tests/plugin/PluginStructureTest.res.mjs +86 -8
  45. package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
  46. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
  47. package/tests/plugin/StateViewSlice/PsCategoriesView.res +17 -0
  48. package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +57 -0
  49. package/tests/plugin/StateViewSlice/PsGatedView.res +15 -0
  50. package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +60 -0
  51. package/tests/plugin/pluginDefinitionRequiredScalars.txt +4 -0
@@ -27,22 +27,41 @@ function isIdsFieldName(name) {
27
27
  }
28
28
  }
29
29
 
30
+ let semanticCompositeNames = [
31
+ [
32
+ Semantic$Reventless.Id.money,
33
+ "Money"
34
+ ],
35
+ [
36
+ Semantic$Reventless.Id.dateRange,
37
+ "DateRange"
38
+ ],
39
+ [
40
+ Semantic$Reventless.Id.geoPoint,
41
+ "GeoPoint"
42
+ ]
43
+ ];
44
+
45
+ function canonicalName(id) {
46
+ return Stdlib_Option.map(semanticCompositeNames.find(param => param[0] === id), param => param[1]);
47
+ }
48
+
30
49
  function fromSury(parentName, fieldName, schema) {
31
- let shape = shapeOf(parentName, fieldName, schema);
32
50
  let sem = Semantic$Reventless.get(schema);
33
51
  if (sem === undefined) {
34
- return shape;
52
+ return shapeOf(parentName, fieldName, schema);
35
53
  }
36
54
  let id = sem.id;
37
- if (id !== Semantic$Reventless.Id.dateTime && id !== Semantic$Reventless.Id.reference) {
38
- return {
39
- TAG: "Semantic",
40
- _0: sem,
41
- _1: shape
42
- };
43
- } else {
44
- return shape;
55
+ if (id === Semantic$Reventless.Id.dateTime || id === Semantic$Reventless.Id.reference) {
56
+ return shapeOf(parentName, fieldName, schema);
45
57
  }
58
+ let name = canonicalName(id);
59
+ let shape = name !== undefined ? shapeOf(name, "", schema) : shapeOf(parentName, fieldName, schema);
60
+ return {
61
+ TAG: "Semantic",
62
+ _0: sem,
63
+ _1: shape
64
+ };
46
65
  }
47
66
 
48
67
  function shapeOf(parentName, fieldName, schema) {
@@ -195,6 +214,8 @@ export {
195
214
  isDateTime,
196
215
  isIdFieldName,
197
216
  isIdsFieldName,
217
+ semanticCompositeNames,
218
+ canonicalName,
198
219
  fromSury,
199
220
  shapeOf,
200
221
  optionalFieldNames,
@@ -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 ────────────────────────────────────
@@ -650,6 +698,12 @@ let make = (
650
698
  let qf = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
651
699
  let stateSchema = R.Spec.stateSchema->S.castToUnknown
652
700
  let label = labelFieldsFromStateSchema(~entityName=R.Spec.name, stateSchema)
701
+ // The same call the capability deriver makes, so the published key and the
702
+ // key the generated filter/order-by is built from cannot disagree.
703
+ let keyField = GraphQL_FragmentGenerator.resolveKeyField(
704
+ ~entityName=R.Spec.name,
705
+ stateSchema,
706
+ )
653
707
  // The events this read model projects from, qualified to the plugin's event ids so
654
708
  // they match the producers' nodes in the graph. Was empty — which dropped projection
655
709
  // edges for any event reaching the read model via a DCB-log-sourced mapping (a classic
@@ -665,8 +719,18 @@ let make = (
665
719
  searchableFields: label.searchableFields,
666
720
  labelFieldSource: Some(labelFieldSourceToString(label.source)),
667
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),
668
725
  visibility: visibilityTag(R.Spec.visibility),
669
726
  chapter: chapterOf(R.Spec.name),
727
+ // Taken from the `qf` record, never re-derived: `Api_Naming` is the only
728
+ // place that decides how `Products`/`Categories` singularise, and the
729
+ // point of publishing the name is that a consumer stops guessing it.
730
+ singleQueryField: Some(qf.singleFieldName),
731
+ idField: keyField->Option.map(((f, _)) => f),
732
+ idFieldSource: keyField->Option.map(((_, rung)) => rung),
733
+ requiredAccess: accessKeysFor(R.Spec.authorization),
670
734
  }: Reventless.Plugin.queryableDef)
671
735
  })
672
736
 
@@ -676,6 +740,10 @@ let make = (
676
740
  let (_, consumed) = svsConsumed->Array.getUnsafe(i)
677
741
  let stateSchema = SVS.Spec.stateSchema->S.castToUnknown
678
742
  let label = labelFieldsFromStateSchema(~entityName=SVS.Spec.name, stateSchema)
743
+ let keyField = GraphQL_FragmentGenerator.resolveKeyField(
744
+ ~entityName=SVS.Spec.name,
745
+ stateSchema,
746
+ )
679
747
  ({
680
748
  Reventless.Plugin.name: SVS.Spec.name,
681
749
  queryField: qf.listFieldName,
@@ -686,8 +754,13 @@ let make = (
686
754
  searchableFields: label.searchableFields,
687
755
  labelFieldSource: Some(labelFieldSourceToString(label.source)),
688
756
  statusField: statusFieldFromStateSchema(~entityName=SVS.Spec.name, stateSchema),
757
+ ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
689
758
  visibility: visibilityTag(SVS.Spec.visibility),
690
759
  chapter: chapterOf(SVS.Spec.name),
760
+ singleQueryField: Some(qf.singleFieldName),
761
+ idField: keyField->Option.map(((f, _)) => f),
762
+ idFieldSource: keyField->Option.map(((_, rung)) => rung),
763
+ requiredAccess: accessKeysFor(SVS.Spec.authorization),
691
764
  }: Reventless.Plugin.queryableDef)
692
765
  })
693
766
 
@@ -708,6 +781,7 @@ let make = (
708
781
  ~commandSchema=SCS.Spec.commandSchema->S.castToUnknown,
709
782
  ~variant=variantName,
710
783
  ),
784
+ ~commandAuthorization=SCS.Spec.commandAuthorization->Obj.magic,
711
785
  SCS.Spec.commandSchema->S.castToUnknown,
712
786
  ),
713
787
  producedEventTypes: produced,
@@ -731,6 +805,7 @@ let make = (
731
805
  commands: extractCommandDefs(
732
806
  ~isAggregate=true,
733
807
  ~mutationFieldFor=variantName => Api_Naming.aggregateMutationField(~plugin=name, ~aggregate=A.Spec.name, ~command=variantName),
808
+ ~commandAuthorization=A.Spec.commandAuthorization->Obj.magic,
734
809
  A.Spec.commandSchema->S.castToUnknown,
735
810
  ),
736
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";
@@ -19,6 +20,7 @@ import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJso
19
20
  import * as Capability_Inference$ReventlessCore from "./Capability_Inference.res.mjs";
20
21
  import * as ApiTargetStateHelpers$ReventlessCore from "../../components/Api/ApiTargetStateHelpers.res.mjs";
21
22
  import * as ApiAllowedStatesHelpers$ReventlessCore from "../../components/Api/ApiAllowedStatesHelpers.res.mjs";
23
+ import * as GraphQL_FragmentGenerator$ReventlessCore from "../../components/Api/GraphQL_FragmentGenerator.res.mjs";
22
24
 
23
25
  let log = Logger$ReventlessCore.fromEnv();
24
26
 
@@ -268,7 +270,16 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
268
270
  ];
269
271
  }
270
272
  };
271
- 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) => {
272
283
  let mkDef = (variantName, properties) => {
273
284
  let match = commandLevelAndId(isAggregate, variantName, properties);
274
285
  let references = extractReferences(properties);
@@ -279,6 +290,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
279
290
  let excluded = ApiNoApiHelpers$ReventlessCore.getExcludedVariants(parentSchema);
280
291
  apiExposed = excluded !== undefined ? !excluded.has(variantName) : true;
281
292
  }
293
+ let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
294
+ TAG: variantName
295
+ }) : variantName;
296
+ let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand));
282
297
  return {
283
298
  name: variantName,
284
299
  schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
@@ -288,7 +303,9 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
288
303
  references: references,
289
304
  allowedStates: allowedStates,
290
305
  targetState: targetState,
291
- apiExposed: apiExposed
306
+ apiExposed: apiExposed,
307
+ requiredAccess: requiredAccess,
308
+ ownerField: Owner$Reventless.fieldNamesOfProperties(properties)[0]
292
309
  };
293
310
  };
294
311
  switch (v.type) {
@@ -314,11 +331,11 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
314
331
  return;
315
332
  }
316
333
  };
317
- let extractCommandDefs = (isAggregate, mutationFieldFor, commandSchema) => {
334
+ let extractCommandDefs = (isAggregate, mutationFieldFor, commandAuthorization, commandSchema) => {
318
335
  if (commandSchema.type === "union") {
319
- 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));
320
337
  } else {
321
- return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandSchema), [], def => [def]);
338
+ return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandSchema), [], def => [def]);
322
339
  }
323
340
  };
324
341
  let scsProduced = stateChangeSlices.map(SCS => [
@@ -500,6 +517,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
500
517
  let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(name, R.Spec.name, undefined);
501
518
  let stateSchema = R.Spec.stateSchema;
502
519
  let label = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
520
+ let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(R.Spec.name, stateSchema);
503
521
  let consumed = qualify(name, R.consumedEventNames);
504
522
  return {
505
523
  name: R.Spec.name,
@@ -511,8 +529,13 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
511
529
  searchableFields: label.searchableFields,
512
530
  labelFieldSource: labelFieldSourceToString(label.source),
513
531
  statusField: statusFieldFromStateSchema(R.Spec.name, stateSchema),
532
+ ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
514
533
  visibility: visibilityTag(R.Spec.visibility),
515
- chapter: componentChapters[R.Spec.name]
534
+ chapter: componentChapters[R.Spec.name],
535
+ singleQueryField: qf.singleFieldName,
536
+ idField: Stdlib_Option.map(keyField, param => param[0]),
537
+ idFieldSource: Stdlib_Option.map(keyField, param => param[1]),
538
+ requiredAccess: accessKeysFor(R.Spec.authorization)
516
539
  };
517
540
  });
518
541
  let stateViewDefs = stateViewSlices.map((SVS, i) => {
@@ -521,6 +544,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
521
544
  let consumed = match[1];
522
545
  let stateSchema = SVS.Spec.stateSchema;
523
546
  let label = labelFieldsFromStateSchema(SVS.Spec.name, stateSchema);
547
+ let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(SVS.Spec.name, stateSchema);
524
548
  return {
525
549
  name: SVS.Spec.name,
526
550
  queryField: qf.listFieldName,
@@ -531,8 +555,13 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
531
555
  searchableFields: label.searchableFields,
532
556
  labelFieldSource: labelFieldSourceToString(label.source),
533
557
  statusField: statusFieldFromStateSchema(SVS.Spec.name, stateSchema),
558
+ ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
534
559
  visibility: visibilityTag(SVS.Spec.visibility),
535
- chapter: componentChapters[SVS.Spec.name]
560
+ chapter: componentChapters[SVS.Spec.name],
561
+ singleQueryField: qf.singleFieldName,
562
+ idField: Stdlib_Option.map(keyField, param => param[0]),
563
+ idFieldSource: Stdlib_Option.map(keyField, param => param[1]),
564
+ requiredAccess: accessKeysFor(SVS.Spec.authorization)
536
565
  };
537
566
  });
538
567
  let stateChangeDefs = stateChangeSlices.map((SCS, i) => {
@@ -542,7 +571,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
542
571
  let consumed = match$1[1];
543
572
  return {
544
573
  name: SCS.Spec.name,
545
- 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),
546
575
  producedEventTypes: produced,
547
576
  consumedEventTypes: consumed,
548
577
  linkedViews: linkedSvsFor(produced),
@@ -557,7 +586,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
557
586
  let produced = match[1];
558
587
  return {
559
588
  name: A.Spec.name,
560
- 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),
561
590
  producedEventTypes: produced,
562
591
  consumedEventTypes: [],
563
592
  linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),