@reventlessdev/reventless-core 3.0.0-alpha.222 → 3.0.0-alpha.224

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 (30) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/package.json +7 -7
  3. package/src/admin/Platform_Admin_Structure.res +10 -0
  4. package/src/admin/Platform_Admin_Structure.res.mjs +6 -1
  5. package/src/admin/Platform_ComponentDefinitionsApi.res +33 -2
  6. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +18 -2
  7. package/src/components/Api/Api_Ids.res +63 -0
  8. package/src/components/Api/Api_Ids.res.mjs +43 -0
  9. package/src/components/Api/GraphQL_FragmentGenerator.res +121 -28
  10. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +97 -27
  11. package/src/components/Api/QueryDbListQuery.res +13 -5
  12. package/src/components/Api/QueryDbListQuery.res.mjs +5 -3
  13. package/src/components/Api/SchemaType.res +36 -2
  14. package/src/components/Api/SchemaType.res.mjs +31 -10
  15. package/src/plugin/component/Plugin_Structure.res +41 -24
  16. package/src/plugin/component/Plugin_Structure.res.mjs +25 -18
  17. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +118 -7
  18. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +168 -7
  19. package/tests/admin/Platform_PluginStructuresApiTest.res +10 -0
  20. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +23 -2
  21. package/tests/api/Api_IdsTest.res +143 -0
  22. package/tests/api/Api_IdsTest.res.mjs +97 -0
  23. package/tests/api/GraphQL_FragmentGeneratorTest.res +169 -0
  24. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +179 -0
  25. package/tests/plugin/PluginStructureTest.res +159 -0
  26. package/tests/plugin/PluginStructureTest.res.mjs +160 -6
  27. package/tests/plugin/StateChangeSlice/PsReserveStock.res +44 -0
  28. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +67 -0
  29. package/tests/plugin/StateViewSlice/PsCategoriesView.res +17 -0
  30. package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +57 -0
@@ -3,6 +3,7 @@
3
3
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
6
+ import * as Api_Naming$ReventlessCore from "./Api_Naming.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";
8
9
  import * as GraphQL_Stitcher$ReventlessCore from "./GraphQL_Stitcher.res.mjs";
@@ -72,7 +73,20 @@ function fromSchemaType(_required, _asInputOpt, _st, collectedTypes, seenTypes)
72
73
  }
73
74
  return name$1 + bang;
74
75
  case "Semantic" :
75
- _st = st._1;
76
+ let inner = st._1;
77
+ if (typeof inner === "object" && inner.TAG === "ObjectRef") {
78
+ let name$2 = inner._0;
79
+ if (Stdlib_Option.isSome(SchemaType$ReventlessCore.canonicalName(st._0.id))) {
80
+ let typeName = asInput ? name$2 + "Input" : name$2;
81
+ if (!seenTypes.has(typeName)) {
82
+ seenTypes.add(typeName);
83
+ let typeDef$1 = objectRefToGraphQL(asInput, typeName, inner._1, collectedTypes, seenTypes);
84
+ collectedTypes.push(typeDef$1);
85
+ }
86
+ return typeName + bang;
87
+ }
88
+ }
89
+ _st = inner;
76
90
  _asInputOpt = asInput;
77
91
  continue;
78
92
  }
@@ -172,11 +186,55 @@ function scalarOfSchemaType(_st) {
172
186
  };
173
187
  }
174
188
 
175
- function deriveServerCapability(schema) {
176
- let spec = StateAnnotations$Reventless.getSpec(schema);
177
- if (spec === undefined) {
178
- return emptyCapability;
189
+ function isKeyFieldName(name) {
190
+ if (name.length > 2) {
191
+ return name.endsWith("Id");
192
+ } else {
193
+ return false;
194
+ }
195
+ }
196
+
197
+ function resolveKeyField(entityName, schema) {
198
+ let match = StateAnnotations$Reventless.getSpec(schema);
199
+ let declared = match !== undefined ? match.ids[0] : undefined;
200
+ if (declared !== undefined) {
201
+ return [
202
+ declared,
203
+ "annotation"
204
+ ];
205
+ }
206
+ let candidates = Object.keys(Stdlib_Option.getOr(SchemaType$ReventlessCore.fromSuryObject("", schema), {})).filter(isKeyFieldName);
207
+ let singular = Api_Naming$ReventlessCore.singularize(Api_Naming$ReventlessCore.stripViewSuffix(entityName));
208
+ let conventional = singular.slice(0, 1).toLowerCase() + singular.slice(1, singular.length) + "Id";
209
+ if (candidates.includes(conventional)) {
210
+ return [
211
+ conventional,
212
+ "convention"
213
+ ];
214
+ } else if (candidates.length === 1) {
215
+ return [
216
+ candidates[0],
217
+ "sole"
218
+ ];
219
+ } else {
220
+ return;
221
+ }
222
+ }
223
+
224
+ function entityNameOf(entry) {
225
+ let n = entry.specName;
226
+ if (n !== undefined) {
227
+ return n;
228
+ }
229
+ let i = entry.returnTypeName.lastIndexOf("_");
230
+ if (i !== -1) {
231
+ return entry.returnTypeName.slice(i + 1 | 0, entry.returnTypeName.length);
232
+ } else {
233
+ return entry.returnTypeName;
179
234
  }
235
+ }
236
+
237
+ function deriveServerCapability(entityName, schema) {
180
238
  let fieldTypes = Stdlib_Option.getOr(SchemaType$ReventlessCore.fromSuryObject("", schema), {});
181
239
  let scalarOf = fieldName => Stdlib_Option.mapOr(fieldTypes[fieldName], "String", scalarOfSchemaType);
182
240
  let filterFields = [];
@@ -201,26 +259,35 @@ function deriveServerCapability(schema) {
201
259
  return;
202
260
  }
203
261
  };
204
- spec.ids.forEach(name => {
205
- pushFilter(name, false);
206
- pushSort(name);
207
- });
208
- spec.compositeIds.forEach(name => pushFilter(name, false));
209
- spec.subIds.forEach(name => {
210
- pushFilter(name, true);
211
- pushSort(name);
212
- });
213
- spec.compositeSubIds.forEach(name => {
214
- pushFilter(name, true);
215
- pushSort(name);
216
- });
217
- spec.indexes.forEach(param => {
218
- let name = param[0];
219
- pushFilter(name, false);
220
- pushSort(name);
221
- });
222
- spec.scan.forEach(name => pushFilter(name, false));
223
- spec.scanSort.forEach(pushSort);
262
+ let spec = StateAnnotations$Reventless.getSpec(schema);
263
+ if (spec !== undefined) {
264
+ spec.ids.forEach(name => {
265
+ pushFilter(name, false);
266
+ pushSort(name);
267
+ });
268
+ spec.compositeIds.forEach(name => pushFilter(name, false));
269
+ spec.subIds.forEach(name => {
270
+ pushFilter(name, true);
271
+ pushSort(name);
272
+ });
273
+ spec.compositeSubIds.forEach(name => {
274
+ pushFilter(name, true);
275
+ pushSort(name);
276
+ });
277
+ spec.indexes.forEach(param => {
278
+ let name = param[0];
279
+ pushFilter(name, false);
280
+ pushSort(name);
281
+ });
282
+ spec.scan.forEach(name => pushFilter(name, false));
283
+ spec.scanSort.forEach(pushSort);
284
+ }
285
+ let match = resolveKeyField(entityName, schema);
286
+ if (match !== undefined) {
287
+ let field = match[0];
288
+ pushFilter(field, false);
289
+ pushSort(field);
290
+ }
224
291
  return {
225
292
  filterFields: filterFields,
226
293
  sortFields: sortFields
@@ -435,7 +502,7 @@ function generate(mutationEntries, queryEntries) {
435
502
  types.push(t);
436
503
  });
437
504
  }
438
- let capability = deriveServerCapability(entry.stateSchema);
505
+ let capability = deriveServerCapability(entityNameOf(entry), entry.stateSchema);
439
506
  let connectionFilterTypeName = entry.returnTypeName + "Filter";
440
507
  if (!seenTypes.has(connectionFilterTypeName)) {
441
508
  seenTypes.add(connectionFilterTypeName);
@@ -491,6 +558,9 @@ export {
491
558
  deriveSubIdFilterType,
492
559
  emptyCapability,
493
560
  scalarOfSchemaType,
561
+ isKeyFieldName,
562
+ resolveKeyField,
563
+ entityNameOf,
494
564
  deriveServerCapability,
495
565
  validateScanSortAlignment,
496
566
  deriveConnectionFilterType,
@@ -503,4 +573,4 @@ export {
503
573
  deriveMutationFieldFromObject,
504
574
  generate,
505
575
  }
506
- /* SchemaType-ReventlessCore Not a pure module */
576
+ /* Api_Naming-ReventlessCore Not a pure module */
@@ -71,15 +71,16 @@ let buildConnection = (
71
71
  }
72
72
 
73
73
  // Compute the Relay connection response (edges + pageInfo) for `items` under the
74
- // GraphQL args. `decodeLocalId` maps a possibly-Relay-encoded id to its local id
75
- // (the resolver passes DomainGraphQL_Server.decodeGlobalId); everything else is
76
- // pure over the item array.
74
+ // GraphQL args. `decodeLocalId` maps a possibly-Relay-encoded id to its local id;
75
+ // it defaults to the shared rule so every caller applies the same one — the AWS
76
+ // Lambda used to pass a no-op here, which silently made `filter.ids` reject a
77
+ // global id on that provider only. Everything else is pure over the item array.
77
78
  let run = (
78
79
  ~items: array<JSON.t>,
79
80
  ~argsDict: Dict.t<JSON.t>,
80
81
  ~capability: GraphQL_FragmentGenerator.serverCapability,
81
82
  ~labelField: string,
82
- ~decodeLocalId: string => option<string>,
83
+ ~decodeLocalId: string => option<string>=Api_Ids.toLocalId,
83
84
  ): JSON.t => {
84
85
  let filterDict =
85
86
  argsDict->Dict.get("filter")->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
@@ -145,11 +146,18 @@ let run = (
145
146
  getLabel(item)->String.toLowerCase->String.startsWith(p->String.toLowerCase)
146
147
  | _ => true
147
148
  }
149
+ // Either id form matches, in both directions: the argument may be a Relay
150
+ // global id while the row carries its storage key (the normal case — every
151
+ // door but `node` reports the storage key), and the row may carry a global id
152
+ // where some caller stamped one. Decoding only the row's id, as this did,
153
+ // covered just the second.
148
154
  let passIds = switch ids {
149
155
  | Some(idList) if idList->Array.length > 0 =>
150
156
  let itemId = getId(item)
151
157
  let itemLocalId = decodeLocalId(itemId)
152
- idList->Array.some(i => i == itemId || itemLocalId == Some(i))
158
+ idList->Array.some(i =>
159
+ i == itemId || itemLocalId == Some(i) || decodeLocalId(i) == Some(itemId)
160
+ )
153
161
  | _ => true
154
162
  }
155
163
  let passPerField = perFieldChecks->Array.every(check => check(item))
@@ -5,6 +5,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
7
7
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
+ import * as Api_Ids$ReventlessCore from "./Api_Ids.res.mjs";
8
9
 
9
10
  function encodeCursor(value) {
10
11
  return btoa(value);
@@ -47,7 +48,8 @@ function buildConnection(pageItems, hasNextPage, hasPreviousPage, cursorValueOf)
47
48
  };
48
49
  }
49
50
 
50
- function run(items, argsDict, capability, labelField, decodeLocalId) {
51
+ function run(items, argsDict, capability, labelField, decodeLocalIdOpt) {
52
+ let decodeLocalId = decodeLocalIdOpt !== undefined ? decodeLocalIdOpt : Api_Ids$ReventlessCore.toLocalId;
51
53
  let filterDict = Stdlib_Option.getOr(Stdlib_Option.flatMap(argsDict["filter"], Stdlib_JSON.Decode.object), {});
52
54
  let search = Stdlib_Option.flatMap(filterDict["search"], Stdlib_JSON.Decode.string);
53
55
  let searchPrefix = Stdlib_Option.flatMap(filterDict["searchPrefix"], Stdlib_JSON.Decode.string);
@@ -83,10 +85,10 @@ function run(items, argsDict, capability, labelField, decodeLocalId) {
83
85
  let itemId = getId(item);
84
86
  let itemLocalId = decodeLocalId(itemId);
85
87
  passIds = ids.some(i => {
86
- if (i === itemId) {
88
+ if (i === itemId || Primitive_object.equal(itemLocalId, i)) {
87
89
  return true;
88
90
  } else {
89
- return Primitive_object.equal(itemLocalId, i);
91
+ return Primitive_object.equal(decodeLocalId(i), itemId);
90
92
  }
91
93
  });
92
94
  } else {
@@ -32,8 +32,34 @@ let isIdsFieldName = (name: string): bool => {
32
32
  len > 3 && String.endsWith(lower, "ids")
33
33
  }
34
34
 
35
+ /**
36
+ The GraphQL type name a semantic **composite** is emitted under.
37
+
38
+ The branded scalars are absent on purpose: they refine a `string` or a number
39
+ without changing its shape, so each is already a `String` or a `Float` in GraphQL
40
+ and has no definition that could be duplicated. Only the semantics that turn a
41
+ field into an object or an enum need a name of their own — without one, the same
42
+ type is emitted once per *field* that uses it, under the field path that reached
43
+ it (`Catalog_AddProductPriceCurrency`, `Ordering_SyncNewProductPriceCurrency`, …
44
+ six copies of ISO 4217 in one example schema).
45
+
46
+ Naming them is what makes the definitions identical across plugins, which is the
47
+ property merged-API composition needs: AppSync unions same-named types from
48
+ different source APIs, and a union of identical definitions is that definition.
49
+ The `Node` / `PageInfo` / `SortOrder` base types already rely on exactly this.
50
+ */
51
+ let semanticCompositeNames = [
52
+ (Reventless.Semantic.Id.money, "Money"),
53
+ (Reventless.Semantic.Id.dateRange, "DateRange"),
54
+ (Reventless.Semantic.Id.geoPoint, "GeoPoint"),
55
+ ]
56
+
57
+ let canonicalName = (id: string): option<string> =>
58
+ semanticCompositeNames
59
+ ->Array.find(((semanticId, _)) => semanticId == id)
60
+ ->Option.map(((_, name)) => name)
61
+
35
62
  let rec fromSury = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): schemaType => {
36
- let shape = shapeOf(~parentName, ~fieldName, schema)
37
63
  // Semantics the IR already has a dedicated shape for keep it: `dateTime` and
38
64
  // `reference` are read below via `isDateTime` / `getTarget`, both of which now
39
65
  // consult the generic marker, and their `format` output is a published
@@ -42,8 +68,16 @@ let rec fromSury = (~parentName: string, ~fieldName: string, schema: S.t<unknown
42
68
  switch Reventless.Semantic.get(schema) {
43
69
  | Some({id} as sem)
44
70
  if id !== Reventless.Semantic.Id.dateTime && id !== Reventless.Semantic.Id.reference =>
71
+ // A composite is one type wherever it appears, so its shape is walked under
72
+ // its own name rather than under the field path that reached it. That
73
+ // renames what is nested inside it too — `Money`'s currency enum becomes
74
+ // `MoneyCurrency` for every field, rather than one enum per price field.
75
+ let shape = switch canonicalName(id) {
76
+ | Some(name) => shapeOf(~parentName=name, ~fieldName="", schema)
77
+ | None => shapeOf(~parentName, ~fieldName, schema)
78
+ }
45
79
  Semantic(sem, shape)
46
- | _ => shape
80
+ | _ => shapeOf(~parentName, ~fieldName, schema)
47
81
  }
48
82
  }
49
83
 
@@ -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,
@@ -164,20 +164,29 @@ let labelFieldsFromStateSchema = (
164
164
  // because its components never pass through `make` — has to derive its defs the
165
165
  // same way, and a second copy of this walk would be free to drift from this one.
166
166
 
167
+ // A variant's declared cross-entity references. Shared by the command and event
168
+ // walks because the two ask the identical question of identical field dicts, and
169
+ // the question is `getFieldTarget` rather than `getTarget`: a reference declared
170
+ // on an `array<string>` field sits on the element schema. Collecting it in one
171
+ // place is what stops one walk from being taught that and the other not.
172
+ let extractReferences = (properties: dict<S.t<unknown>>): array<
173
+ Reventless.Plugin.fieldReference,
174
+ > =>
175
+ properties
176
+ ->Dict.toArray
177
+ ->Array.filterMap(((fieldName, fieldSchema)) =>
178
+ Reventless.Reference.getFieldTarget(fieldSchema)->Option.map(target => (
179
+ {
180
+ Reventless.Plugin.fieldName,
181
+ entity: target.entity,
182
+ plugin: target.plugin,
183
+ }: Reventless.Plugin.fieldReference
184
+ ))
185
+ )
186
+
167
187
  let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
168
188
  let mkDef = (~variantName, ~properties) => {
169
- let references =
170
- properties
171
- ->Dict.toArray
172
- ->Array.filterMap(((fieldName, fieldSchema)) =>
173
- Reventless.Reference.getTarget(fieldSchema)->Option.map(target => (
174
- {
175
- Reventless.Plugin.fieldName,
176
- entity: target.entity,
177
- plugin: target.plugin,
178
- }: Reventless.Plugin.fieldReference
179
- ))
180
- )
189
+ let references = extractReferences(properties)
181
190
  ({
182
191
  Reventless.Plugin.name: variantName,
183
192
  // Derived for the same reason as `commandDef.schema`: an event's field
@@ -301,18 +310,7 @@ let make = (
301
310
  // `S.literal("Archive")` string rather than an `{TAG, ...}` object.
302
311
  let mkDef = (~variantName, ~properties) => {
303
312
  let (level, aggregateIdField) = commandLevelAndId(~isAggregate, ~variantName, properties)
304
- let references =
305
- properties
306
- ->Dict.toArray
307
- ->Array.filterMap(((fieldName, fieldSchema)) =>
308
- Reventless.Reference.getTarget(fieldSchema)->Option.map(target => (
309
- {
310
- Reventless.Plugin.fieldName,
311
- entity: target.entity,
312
- plugin: target.plugin,
313
- }: Reventless.Plugin.fieldReference
314
- ))
315
- )
313
+ let references = extractReferences(properties)
316
314
  // Per-variant `allowedStates` lives on the *parent* command schema
317
315
  // (the PPX attaches a single dict<variantName, [|states|]> via
318
316
  // markAllowedStates). Look it up by variant name; back-compat
@@ -652,6 +650,12 @@ let make = (
652
650
  let qf = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
653
651
  let stateSchema = R.Spec.stateSchema->S.castToUnknown
654
652
  let label = labelFieldsFromStateSchema(~entityName=R.Spec.name, stateSchema)
653
+ // The same call the capability deriver makes, so the published key and the
654
+ // key the generated filter/order-by is built from cannot disagree.
655
+ let keyField = GraphQL_FragmentGenerator.resolveKeyField(
656
+ ~entityName=R.Spec.name,
657
+ stateSchema,
658
+ )
655
659
  // The events this read model projects from, qualified to the plugin's event ids so
656
660
  // they match the producers' nodes in the graph. Was empty — which dropped projection
657
661
  // edges for any event reaching the read model via a DCB-log-sourced mapping (a classic
@@ -669,6 +673,12 @@ let make = (
669
673
  statusField: statusFieldFromStateSchema(~entityName=R.Spec.name, stateSchema),
670
674
  visibility: visibilityTag(R.Spec.visibility),
671
675
  chapter: chapterOf(R.Spec.name),
676
+ // Taken from the `qf` record, never re-derived: `Api_Naming` is the only
677
+ // place that decides how `Products`/`Categories` singularise, and the
678
+ // point of publishing the name is that a consumer stops guessing it.
679
+ singleQueryField: Some(qf.singleFieldName),
680
+ idField: keyField->Option.map(((f, _)) => f),
681
+ idFieldSource: keyField->Option.map(((_, rung)) => rung),
672
682
  }: Reventless.Plugin.queryableDef)
673
683
  })
674
684
 
@@ -678,6 +688,10 @@ let make = (
678
688
  let (_, consumed) = svsConsumed->Array.getUnsafe(i)
679
689
  let stateSchema = SVS.Spec.stateSchema->S.castToUnknown
680
690
  let label = labelFieldsFromStateSchema(~entityName=SVS.Spec.name, stateSchema)
691
+ let keyField = GraphQL_FragmentGenerator.resolveKeyField(
692
+ ~entityName=SVS.Spec.name,
693
+ stateSchema,
694
+ )
681
695
  ({
682
696
  Reventless.Plugin.name: SVS.Spec.name,
683
697
  queryField: qf.listFieldName,
@@ -690,6 +704,9 @@ let make = (
690
704
  statusField: statusFieldFromStateSchema(~entityName=SVS.Spec.name, stateSchema),
691
705
  visibility: visibilityTag(SVS.Spec.visibility),
692
706
  chapter: chapterOf(SVS.Spec.name),
707
+ singleQueryField: Some(qf.singleFieldName),
708
+ idField: keyField->Option.map(((f, _)) => f),
709
+ idFieldSource: keyField->Option.map(((_, rung)) => rung),
693
710
  }: Reventless.Plugin.queryableDef)
694
711
  })
695
712
 
@@ -19,6 +19,7 @@ import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJso
19
19
  import * as Capability_Inference$ReventlessCore from "./Capability_Inference.res.mjs";
20
20
  import * as ApiTargetStateHelpers$ReventlessCore from "../../components/Api/ApiTargetStateHelpers.res.mjs";
21
21
  import * as ApiAllowedStatesHelpers$ReventlessCore from "../../components/Api/ApiAllowedStatesHelpers.res.mjs";
22
+ import * as GraphQL_FragmentGenerator$ReventlessCore from "../../components/Api/GraphQL_FragmentGenerator.res.mjs";
22
23
 
23
24
  let log = Logger$ReventlessCore.fromEnv();
24
25
 
@@ -140,16 +141,20 @@ function labelFieldsFromStateSchema(entityName, stateSchema) {
140
141
  };
141
142
  }
142
143
 
144
+ function extractReferences(properties) {
145
+ return Stdlib_Array.filterMap(Object.entries(properties), param => {
146
+ let fieldName = param[0];
147
+ return Stdlib_Option.map(Reference$Reventless.getFieldTarget(param[1]), target => ({
148
+ fieldName: fieldName,
149
+ entity: target.entity,
150
+ plugin: target.plugin
151
+ }));
152
+ });
153
+ }
154
+
143
155
  function toEventDef(v) {
144
156
  let mkDef = (variantName, properties) => {
145
- let references = Stdlib_Array.filterMap(Object.entries(properties), param => {
146
- let fieldName = param[0];
147
- return Stdlib_Option.map(Reference$Reventless.getTarget(param[1]), target => ({
148
- fieldName: fieldName,
149
- entity: target.entity,
150
- plugin: target.plugin
151
- }));
152
- });
157
+ let references = extractReferences(properties);
153
158
  return {
154
159
  name: variantName,
155
160
  schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
@@ -267,14 +272,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
267
272
  let toCommandDef = (isAggregate, mutationFieldFor, parentSchema, v) => {
268
273
  let mkDef = (variantName, properties) => {
269
274
  let match = commandLevelAndId(isAggregate, variantName, properties);
270
- let references = Stdlib_Array.filterMap(Object.entries(properties), param => {
271
- let fieldName = param[0];
272
- return Stdlib_Option.map(Reference$Reventless.getTarget(param[1]), target => ({
273
- fieldName: fieldName,
274
- entity: target.entity,
275
- plugin: target.plugin
276
- }));
277
- });
275
+ let references = extractReferences(properties);
278
276
  let allowedStates = ApiAllowedStatesHelpers$ReventlessCore.getAllowedStates(parentSchema, variantName);
279
277
  let targetState = ApiTargetStateHelpers$ReventlessCore.getTargetState(parentSchema, variantName);
280
278
  let apiExposed = false;
@@ -503,6 +501,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
503
501
  let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(name, R.Spec.name, undefined);
504
502
  let stateSchema = R.Spec.stateSchema;
505
503
  let label = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
504
+ let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(R.Spec.name, stateSchema);
506
505
  let consumed = qualify(name, R.consumedEventNames);
507
506
  return {
508
507
  name: R.Spec.name,
@@ -515,7 +514,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
515
514
  labelFieldSource: labelFieldSourceToString(label.source),
516
515
  statusField: statusFieldFromStateSchema(R.Spec.name, stateSchema),
517
516
  visibility: visibilityTag(R.Spec.visibility),
518
- chapter: componentChapters[R.Spec.name]
517
+ chapter: componentChapters[R.Spec.name],
518
+ singleQueryField: qf.singleFieldName,
519
+ idField: Stdlib_Option.map(keyField, param => param[0]),
520
+ idFieldSource: Stdlib_Option.map(keyField, param => param[1])
519
521
  };
520
522
  });
521
523
  let stateViewDefs = stateViewSlices.map((SVS, i) => {
@@ -524,6 +526,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
524
526
  let consumed = match[1];
525
527
  let stateSchema = SVS.Spec.stateSchema;
526
528
  let label = labelFieldsFromStateSchema(SVS.Spec.name, stateSchema);
529
+ let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(SVS.Spec.name, stateSchema);
527
530
  return {
528
531
  name: SVS.Spec.name,
529
532
  queryField: qf.listFieldName,
@@ -535,7 +538,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
535
538
  labelFieldSource: labelFieldSourceToString(label.source),
536
539
  statusField: statusFieldFromStateSchema(SVS.Spec.name, stateSchema),
537
540
  visibility: visibilityTag(SVS.Spec.visibility),
538
- chapter: componentChapters[SVS.Spec.name]
541
+ chapter: componentChapters[SVS.Spec.name],
542
+ singleQueryField: qf.singleFieldName,
543
+ idField: Stdlib_Option.map(keyField, param => param[0]),
544
+ idFieldSource: Stdlib_Option.map(keyField, param => param[1])
539
545
  };
540
546
  });
541
547
  let stateChangeDefs = stateChangeSlices.map((SCS, i) => {
@@ -653,6 +659,7 @@ export {
653
659
  statusFieldFromStateSchema,
654
660
  labelFieldSourceToString,
655
661
  labelFieldsFromStateSchema,
662
+ extractReferences,
656
663
  toEventDef,
657
664
  extractEventDefs,
658
665
  extractErrorDefs,