@reventlessdev/reventless-core 3.0.0-alpha.264 → 3.0.0-alpha.266

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.266 (2026-09-21)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **core:** an aggregate read model is not keyed by a foreign id ([0dfed74](https://github.com/ReventlessDev/reventless-core/commit/0dfed747c4182b1fc58675b42f5d3cb3eb6309c7))
11
+ ### Features
12
+
13
+ * **spec:** one Id type per identity ([203be38](https://github.com/ReventlessDev/reventless-core/commit/203be389b9047db0a7a0294fe3c1c04ba5119965))
14
+ * **spec:** the runtime reads identities ([417c5f6](https://github.com/ReventlessDev/reventless-core/commit/417c5f613b031a08c7849a81c75de6a8bbeb841b))
15
+
16
+
17
+ # 3.0.0-alpha.265 (2026-09-20)
18
+
19
+ **Note:** Version bump only for package @reventlessdev/reventless-core
20
+
21
+
22
+
23
+
24
+
6
25
  # 3.0.0-alpha.264 (2026-09-20)
7
26
 
8
27
  **Note:** Version bump only for package @reventlessdev/reventless-core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.264",
3
+ "version": "3.0.0-alpha.266",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -30,15 +30,15 @@
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.33",
32
32
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
33
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
33
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.12",
34
34
  "@reventlessdev/rescript-node": "2.0.0-alpha.12",
35
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
35
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.21",
36
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.12",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.12",
38
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
39
39
  "@reventlessdev/reventless-interop": "3.0.0-alpha.37",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.137",
41
- "@reventlessdev/reventless-infra": "3.0.0-alpha.165"
40
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.167",
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.139"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
@@ -316,19 +316,32 @@ type keyFieldResolution =
316
316
  | Ambiguous({candidates: array<string>, conventional: string})
317
317
  | NoCandidate
318
318
 
319
+ /** Where a view's row key lives. A read model's row is keyed by the envelope id of
320
+ the events it folds, so a `*Id` field in its state is somebody else's id unless
321
+ it is declared or named as the view's own. A StateViewSlice keys its row by a
322
+ state field. */
323
+ type rowKeying = RowKeyedByEnvelope | RowKeyedByStateField
324
+
319
325
  /**
320
326
  The field that identifies a row, and which rung answered:
321
327
 
322
328
  - `"annotation"` — the state declares `@id`. Nothing outranks it.
323
- - `"convention"` — a field named `<singular entity name>Id` exists
324
- (`Products` → `productId`).
325
- - `"sole"` — the state has exactly one `*Id` field (`AvailableProducts`).
329
+ - `"convention"` — a field keyed `<singular entity name>Id` exists
330
+ (`Products` → `productId`): named so, or typed as that identity.
331
+ - `"sole"` — the state has exactly one `*Id` field (`AvailableProducts`). Only for
332
+ `RowKeyedByStateField`: a read model's lone `*Id` is a foreign id (`Orders` →
333
+ `customerId`), and having none of its own is its ordinary shape, so neither
334
+ that nor several `*Id` fields is a gap.
326
335
 
327
336
  Convention outranks sole so a view carrying one foreign key and no key of its own
328
337
  is not keyed by the foreign key. `resolveKeyField` is this, with both gaps
329
338
  flattened to `None`.
330
339
  */
331
- let classifyKeyField = (~entityName: string, schema: S.t<unknown>): keyFieldResolution => {
340
+ let classifyKeyField = (
341
+ ~rowKeying: rowKeying,
342
+ ~entityName: string,
343
+ schema: S.t<unknown>,
344
+ ): keyFieldResolution => {
332
345
  let declared = switch Reventless.StateAnnotations.getSpec(schema) {
333
346
  | Some({ids}) => ids->Array.get(0)
334
347
  | None => None
@@ -336,29 +349,45 @@ let classifyKeyField = (~entityName: string, schema: S.t<unknown>): keyFieldReso
336
349
  switch declared {
337
350
  | Some(field) => Resolved({field, rung: "annotation"})
338
351
  | None =>
352
+ // A field is a candidate by its name or by its type, and its key is its
353
+ // identity when it has one: `orderId: CustomerId.t` never keys `Orders`, and
354
+ // `order: OrderId.t` does.
355
+ let properties = switch schema {
356
+ | Object({properties}) => properties
357
+ | _ => Dict.make()
358
+ }
359
+ let keyOf = field =>
360
+ properties->Dict.get(field)->Option.flatMap(Reventless.Semantic.identityKey)
339
361
  let candidates =
340
362
  SchemaType.fromSuryObject(~typeName="", schema)
341
363
  ->Option.getOr(Dict.make())
342
364
  ->Dict.keysToArray
343
- ->Array.filter(isKeyFieldName)
365
+ ->Array.filter(field => isKeyFieldName(field) || keyOf(field)->Option.isSome)
344
366
  let singular = entityName->Api_Naming.stripViewSuffix->Api_Naming.singularize
345
367
  let conventional =
346
368
  singular->String.slice(~start=0, ~end=1)->String.toLowerCase ++
347
369
  singular->String.slice(~start=1, ~end=singular->String.length) ++ "Id"
348
- if candidates->Array.includes(conventional) {
349
- Resolved({field: conventional, rung: "convention"})
350
- } else if candidates->Array.length == 1 {
351
- Resolved({field: candidates->Array.getUnsafe(0), rung: "sole"})
352
- } else if Array.length(candidates) == 0 {
353
- NoCandidate
354
- } else {
355
- Ambiguous({candidates, conventional})
370
+ switch candidates->Array.find(field => keyOf(field)->Option.getOr(field) == conventional) {
371
+ | Some(field) => Resolved({field, rung: "convention"})
372
+ | None =>
373
+ if rowKeying == RowKeyedByEnvelope {
374
+ NoCandidate
375
+ } else if candidates->Array.length == 1 {
376
+ Resolved({field: candidates->Array.getUnsafe(0), rung: "sole"})
377
+ } else if Array.length(candidates) == 0 {
378
+ NoCandidate
379
+ } else {
380
+ Ambiguous({candidates, conventional})
381
+ }
356
382
  }
357
383
  }
358
384
  }
359
385
 
360
- let resolveKeyField = (~entityName: string, schema: S.t<unknown>): option<(string, string)> =>
361
- switch classifyKeyField(~entityName, schema) {
386
+ let resolveKeyField = (~rowKeying: rowKeying, ~entityName: string, schema: S.t<unknown>): option<(
387
+ string,
388
+ string,
389
+ )> =>
390
+ switch classifyKeyField(~rowKeying, ~entityName, schema) {
362
391
  | Resolved({field, rung}) => Some((field, rung))
363
392
  | Ambiguous(_) | NoCandidate => None
364
393
  }
@@ -457,7 +486,9 @@ let deriveServerCapability = (~entityName: string, schema: S.t<unknown>): server
457
486
  // a client asked for happened client-side over one page. Its key is knowable
458
487
  // without the annotation in the common cases; take it. Pushed last, and both
459
488
  // pushes dedupe, so a declared `@id` keeps its position and this is a no-op.
460
- switch resolveKeyField(~entityName, schema) {
489
+ // Read as a state-field key for every view: a read model's lone foreign id is
490
+ // not its key, but it stays filterable, since dropping it would break the SDL.
491
+ switch resolveKeyField(~rowKeying=RowKeyedByStateField, ~entityName, schema) {
461
492
  | Some((field, _rung)) =>
462
493
  pushFilter(field, ~range=false)
463
494
  pushSort(field)
@@ -5,6 +5,7 @@ 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
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
7
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
+ import * as Semantic$Reventless from "@reventlessdev/reventless-spec/src/semantic/Semantic.res.mjs";
8
9
  import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
9
10
  import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
10
11
  import * as Api_Naming$ReventlessCore from "./Api_Naming.res.mjs";
@@ -256,7 +257,7 @@ function isKeyFieldName(name) {
256
257
  }
257
258
  }
258
259
 
259
- function classifyKeyField(entityName, schema) {
260
+ function classifyKeyField(rowKeying, entityName, schema) {
260
261
  let match = StateAnnotations$Reventless.getSpec(schema);
261
262
  let declared = match !== undefined ? match.ids[0] : undefined;
262
263
  if (declared !== undefined) {
@@ -266,15 +267,27 @@ function classifyKeyField(entityName, schema) {
266
267
  rung: "annotation"
267
268
  };
268
269
  }
269
- let candidates = Object.keys(Stdlib_Option.getOr(SchemaType$ReventlessCore.fromSuryObject("", schema), {})).filter(isKeyFieldName);
270
+ let properties;
271
+ properties = schema.type === "object" ? schema.properties : ({});
272
+ let keyOf = field => Stdlib_Option.flatMap(properties[field], Semantic$Reventless.identityKey);
273
+ let candidates = Object.keys(Stdlib_Option.getOr(SchemaType$ReventlessCore.fromSuryObject("", schema), {})).filter(field => {
274
+ if (isKeyFieldName(field)) {
275
+ return true;
276
+ } else {
277
+ return Stdlib_Option.isSome(keyOf(field));
278
+ }
279
+ });
270
280
  let singular = Api_Naming$ReventlessCore.singularize(Api_Naming$ReventlessCore.stripViewSuffix(entityName));
271
281
  let conventional = singular.slice(0, 1).toLowerCase() + singular.slice(1, singular.length) + "Id";
272
- if (candidates.includes(conventional)) {
282
+ let field = candidates.find(field => Stdlib_Option.getOr(keyOf(field), field) === conventional);
283
+ if (field !== undefined) {
273
284
  return {
274
285
  TAG: "Resolved",
275
- field: conventional,
286
+ field: field,
276
287
  rung: "convention"
277
288
  };
289
+ } else if (rowKeying === "RowKeyedByEnvelope") {
290
+ return "NoCandidate";
278
291
  } else if (candidates.length === 1) {
279
292
  return {
280
293
  TAG: "Resolved",
@@ -292,8 +305,8 @@ function classifyKeyField(entityName, schema) {
292
305
  }
293
306
  }
294
307
 
295
- function resolveKeyField(entityName, schema) {
296
- let match = classifyKeyField(entityName, schema);
308
+ function resolveKeyField(rowKeying, entityName, schema) {
309
+ let match = classifyKeyField(rowKeying, entityName, schema);
297
310
  if (typeof match !== "object" || match.TAG !== "Resolved") {
298
311
  return;
299
312
  } else {
@@ -373,7 +386,7 @@ function deriveServerCapability(entityName, schema) {
373
386
  spec.scan.forEach(name => pushFilter(name, false));
374
387
  spec.scanSort.forEach(pushSort);
375
388
  }
376
- let match = resolveKeyField(entityName, schema);
389
+ let match = resolveKeyField("RowKeyedByStateField", entityName, schema);
377
390
  if (match !== undefined) {
378
391
  let field = match[0];
379
392
  pushFilter(field, false);
@@ -45,6 +45,14 @@ let isTagged = Reventless.DcbTag.isTagged
45
45
  let isDateTime = Reventless.DateTime.isDateTime
46
46
  let isCalendarDate = Reventless.CalendarDate.isCalendarDate
47
47
 
48
+ // On the schema itself, not through `option<…>` as `Semantic.get` reads: the
49
+ // optional wrapper then reaches the `AnyOf` branch and stays `Nullable`.
50
+ let isIdentity = (schema: S.t<unknown>): bool =>
51
+ switch schema->S.Metadata.get(~id=Reventless.Semantic.semanticId) {
52
+ | Some({id}) => id === Reventless.Semantic.Id.identity
53
+ | None => false
54
+ }
55
+
48
56
  let isIdFieldName = (name: string): bool => {
49
57
  let lower = String.toLowerCase(name)
50
58
  let len = String.length(lower)
@@ -123,12 +131,14 @@ and withoutDefault = (
123
131
  }
124
132
 
125
133
  and shapeOf = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): schemaType => {
126
- // Two independent facts, checked independently. A DCB-tagged field is an
134
+ // Three independent facts, checked independently. A DCB-tagged field is an
127
135
  // entity id because it routes; a reference field is one because it points at
128
- // an entity. Neither implies the other — a `@partitionTag` field carries no
136
+ // an entity; an identity-typed field is one whatever it is called. Neither implies the other — a `@partitionTag` field carries no
129
137
  // reference, and `Reference.toWithoutDcbTag` carries no tag — so collapsing
130
138
  // these into one test would silently reclassify whichever case it dropped.
131
- if isTagged(schema) || Reventless.Reference.getTarget(schema)->Option.isSome {
139
+ if (
140
+ isTagged(schema) || Reventless.Reference.getTarget(schema)->Option.isSome || isIdentity(schema)
141
+ ) {
132
142
  EntityId
133
143
  } else {
134
144
  switch schema {
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Sury from "sury";
3
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
@@ -11,6 +12,15 @@ import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/comp
11
12
  import * as CalendarDate$Reventless from "@reventlessdev/reventless-spec/src/semantic/CalendarDate.res.mjs";
12
13
  import * as FieldDefault$Reventless from "@reventlessdev/reventless-spec/src/components/FieldDefault.res.mjs";
13
14
 
15
+ function isIdentity(schema) {
16
+ let match = Sury.$Metadata_get(schema, Semantic$Reventless.semanticId);
17
+ if (match !== undefined) {
18
+ return match.id === Semantic$Reventless.Id.identity;
19
+ } else {
20
+ return false;
21
+ }
22
+ }
23
+
14
24
  function isIdFieldName(name) {
15
25
  let lower = name.toLowerCase();
16
26
  let len = lower.length;
@@ -87,7 +97,7 @@ function withoutDefault(parentName, fieldName, schema) {
87
97
  }
88
98
 
89
99
  function shapeOf(parentName, fieldName, schema) {
90
- if (DcbTag$Reventless.isTagged(schema) || Stdlib_Option.isSome(Reference$Reventless.getTarget(schema))) {
100
+ if (DcbTag$Reventless.isTagged(schema) || Stdlib_Option.isSome(Reference$Reventless.getTarget(schema)) || isIdentity(schema)) {
91
101
  return "EntityId";
92
102
  }
93
103
  switch (schema.type) {
@@ -425,6 +435,7 @@ export {
425
435
  isTagged,
426
436
  isDateTime,
427
437
  isCalendarDate,
438
+ isIdentity,
428
439
  isIdFieldName,
429
440
  isIdsFieldName,
430
441
  semanticCompositeNames,
@@ -439,4 +450,4 @@ export {
439
450
  trailShape,
440
451
  fromSuryObject,
441
452
  }
442
- /* DcbTag-Reventless Not a pure module */
453
+ /* sury Not a pure module */
@@ -310,11 +310,15 @@ and withSemantic = (fieldSchema: JSON.t, sem: Reventless.Semantic.t): JSON.t =>
310
310
  obj->Dict.set("x-reventless-semantic-source", str("type"))
311
311
  switch sem.payload {
312
312
  | Plain => ()
313
- | ReferenceTo({entity, plugin}) =>
314
- obj->Dict.set(
315
- "x-reventless-semantic-target",
316
- jsonObject(withOptionalPlugin([("entity", str(entity))], plugin)),
317
- )
313
+ | ReferenceTo({entity, plugin, ?identity}) =>
314
+ let pairs = withOptionalPlugin([("entity", str(entity))], plugin)
315
+ let pairs = switch identity {
316
+ | Some(key) => Array.concat(pairs, [("identity", str(key))])
317
+ | None => pairs
318
+ }
319
+ obj->Dict.set("x-reventless-semantic-target", jsonObject(pairs))
320
+ | IdentityOf({key}) =>
321
+ obj->Dict.set("x-reventless-semantic-target", jsonObject([("key", str(key))]))
318
322
  | StoredIn({plugin, store}) =>
319
323
  // An absent plugin means "this plugin's own store" — omit the key rather
320
324
  // than writing an empty string, so a reader never has to tell those apart.
@@ -355,10 +355,16 @@ function withSemantic(fieldSchema, sem) {
355
355
  switch (match.TAG) {
356
356
  case "ReferenceTo" :
357
357
  let match$1 = match._0;
358
- obj["x-reventless-semantic-target"] = Object.fromEntries(withOptionalPlugin([[
358
+ let identity = match$1.identity;
359
+ let pairs = withOptionalPlugin([[
359
360
  "entity",
360
361
  match$1.entity
361
- ]], match$1.plugin));
362
+ ]], match$1.plugin);
363
+ let pairs$1 = identity !== undefined ? pairs.concat([[
364
+ "identity",
365
+ identity
366
+ ]]) : pairs;
367
+ obj["x-reventless-semantic-target"] = Object.fromEntries(pairs$1);
362
368
  break;
363
369
  case "StoredIn" :
364
370
  let match$2 = match._0;
@@ -384,6 +390,12 @@ function withSemantic(fieldSchema, sem) {
384
390
  match$3.field
385
391
  ]], match$3.plugin), "view", match$3.view), "content", match$3.content));
386
392
  break;
393
+ case "IdentityOf" :
394
+ obj["x-reventless-semantic-target"] = Object.fromEntries([[
395
+ "key",
396
+ match.key
397
+ ]]);
398
+ break;
387
399
  }
388
400
  }
389
401
  return obj;
@@ -986,9 +986,12 @@ let queryableDefFromSpec = (
986
986
  ): Reventless.Plugin.queryableDef => {
987
987
  let qf = Api_Naming.queryFieldNamesForReadModel(~plugin, ~name)
988
988
  let label = labelFieldsFromStateSchema(~entityName=name, stateSchema)
989
- // The same call the capability deriver makes, so the published key and the key
990
- // the generated filter/order-by is built from cannot disagree.
991
- let keyField = GraphQL_FragmentGenerator.resolveKeyField(~entityName=name, stateSchema)
989
+ // The published key is always filterable; not every filterable field is the key.
990
+ let keyField = GraphQL_FragmentGenerator.resolveKeyField(
991
+ ~rowKeying=RowKeyedByEnvelope,
992
+ ~entityName=name,
993
+ stateSchema,
994
+ )
992
995
  {
993
996
  Reventless.Plugin.name,
994
997
  queryField: qf.listFieldName,
@@ -1414,8 +1417,8 @@ let make = (
1414
1417
 
1415
1418
  // Warned rather than refused: an unkeyed view still answers, just without a
1416
1419
  // filter or an ordering, and a lint that stops a deploy gets silenced.
1417
- let recordKeyField = (~entityName, stateSchema) =>
1418
- GraphQL_FragmentGenerator.classifyKeyField(~entityName, stateSchema)
1420
+ let recordKeyField = (~rowKeying, ~entityName, stateSchema) =>
1421
+ GraphQL_FragmentGenerator.classifyKeyField(~rowKeying, ~entityName, stateSchema)
1419
1422
  ->GraphQL_FragmentGenerator.keyFieldGapMessage
1420
1423
  ->Option.forEach(why => log.warn(~comp="Plugin_Structure", `${name}/${entityName} ${why}`))
1421
1424
 
@@ -1425,14 +1428,17 @@ let make = (
1425
1428
  let qf = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
1426
1429
  let stateSchema = R.Spec.stateSchema->S.castToUnknown
1427
1430
  let label = labelFieldsFromStateSchema(~entityName=R.Spec.name, stateSchema)
1428
- // The same call the capability deriver makes, so the published key and the
1429
- // key the generated filter/order-by is built from cannot disagree.
1430
- let keyField = GraphQL_FragmentGenerator.resolveKeyField(~entityName=R.Spec.name, stateSchema)
1431
+ // The published key is always filterable; not every filterable field is the key.
1432
+ let keyField = GraphQL_FragmentGenerator.resolveKeyField(
1433
+ ~rowKeying=RowKeyedByEnvelope,
1434
+ ~entityName=R.Spec.name,
1435
+ stateSchema,
1436
+ )
1431
1437
  // Qualified to the plugin's event ids so they match the producers' nodes.
1432
1438
  let consumed = qualify(~prefix=name, R.consumedEventNames)
1433
1439
  recordRetired(~entityName=R.Spec.name, stateSchema)
1434
1440
  recordLifecycle(~entityName=R.Spec.name, stateSchema)
1435
- recordKeyField(~entityName=R.Spec.name, stateSchema)
1441
+ recordKeyField(~rowKeying=RowKeyedByEnvelope, ~entityName=R.Spec.name, stateSchema)
1436
1442
 
1437
1443
  (
1438
1444
  {
@@ -1471,12 +1477,13 @@ let make = (
1471
1477
  let stateSchema = SVS.Spec.stateSchema->S.castToUnknown
1472
1478
  let label = labelFieldsFromStateSchema(~entityName=SVS.Spec.name, stateSchema)
1473
1479
  let keyField = GraphQL_FragmentGenerator.resolveKeyField(
1480
+ ~rowKeying=RowKeyedByStateField,
1474
1481
  ~entityName=SVS.Spec.name,
1475
1482
  stateSchema,
1476
1483
  )
1477
1484
  recordRetired(~entityName=SVS.Spec.name, stateSchema)
1478
1485
  recordLifecycle(~entityName=SVS.Spec.name, stateSchema)
1479
- recordKeyField(~entityName=SVS.Spec.name, stateSchema)
1486
+ recordKeyField(~rowKeying=RowKeyedByStateField, ~entityName=SVS.Spec.name, stateSchema)
1480
1487
 
1481
1488
  (
1482
1489
  {
@@ -706,7 +706,7 @@ function queryableDefFromSpec(plugin, name, stateSchema, authorization, visibili
706
706
  let linkedWriteSide = linkedWriteSideOpt !== undefined ? linkedWriteSideOpt : [];
707
707
  let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(plugin, name, undefined);
708
708
  let label = labelFieldsFromStateSchema(name, stateSchema);
709
- let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(name, stateSchema);
709
+ let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", name, stateSchema);
710
710
  return {
711
711
  name: name,
712
712
  queryField: qf.listFieldName,
@@ -1006,16 +1006,16 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1006
1006
  retiredFailures.push(f);
1007
1007
  });
1008
1008
  };
1009
- let recordKeyField = (entityName, stateSchema) => Stdlib_Option.forEach(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField(entityName, stateSchema)), why => log.warn("Plugin_Structure", undefined, name + `/` + entityName + ` ` + why));
1009
+ let recordKeyField = (rowKeying, entityName, stateSchema) => Stdlib_Option.forEach(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField(rowKeying, entityName, stateSchema)), why => log.warn("Plugin_Structure", undefined, name + `/` + entityName + ` ` + why));
1010
1010
  let readModelDefs = readModels.map(R => {
1011
1011
  let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(name, R.Spec.name, undefined);
1012
1012
  let stateSchema = R.Spec.stateSchema;
1013
1013
  let label = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
1014
- let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(R.Spec.name, stateSchema);
1014
+ let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", R.Spec.name, stateSchema);
1015
1015
  let consumed = qualify(name, R.consumedEventNames);
1016
1016
  recordRetired(R.Spec.name, stateSchema);
1017
1017
  recordLifecycle(R.Spec.name, stateSchema);
1018
- recordKeyField(R.Spec.name, stateSchema);
1018
+ recordKeyField("RowKeyedByEnvelope", R.Spec.name, stateSchema);
1019
1019
  return {
1020
1020
  name: R.Spec.name,
1021
1021
  queryField: qf.listFieldName,
@@ -1044,10 +1044,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1044
1044
  let consumed = match[1];
1045
1045
  let stateSchema = SVS.Spec.stateSchema;
1046
1046
  let label = labelFieldsFromStateSchema(SVS.Spec.name, stateSchema);
1047
- let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(SVS.Spec.name, stateSchema);
1047
+ let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", SVS.Spec.name, stateSchema);
1048
1048
  recordRetired(SVS.Spec.name, stateSchema);
1049
1049
  recordLifecycle(SVS.Spec.name, stateSchema);
1050
- recordKeyField(SVS.Spec.name, stateSchema);
1050
+ recordKeyField("RowKeyedByStateField", SVS.Spec.name, stateSchema);
1051
1051
  return {
1052
1052
  name: SVS.Spec.name,
1053
1053
  queryField: qf.listFieldName,
@@ -1,5 +1,12 @@
1
1
  open JestGlobals
2
2
 
3
+ module OrderId = Reventless.Id.Make({
4
+ let key = "orderId"
5
+ })
6
+ module CustomerId = Reventless.Id.Make({
7
+ let key = "customerId"
8
+ })
9
+
3
10
  // A multi-variant command union serializes as a sury `Union`, the same shape an
4
11
  // aggregate command produces. The generator must decide whether to inject the
5
12
  // aggregate-style `id: ID!` argument from the entry's `injectIdArg` flag, not
@@ -94,7 +101,11 @@ describe("GraphQL_FragmentGenerator slice TODO rows", () => {
94
101
  // common cases; these pin which cases those are, and which still need `@id`.
95
102
  describe("resolveKeyField — the ladder", () => {
96
103
  let rungFor = (~entityName, schema) =>
97
- GraphQL_FragmentGenerator.resolveKeyField(~entityName, schema->S.castToUnknown)
104
+ GraphQL_FragmentGenerator.resolveKeyField(
105
+ ~rowKeying=RowKeyedByStateField,
106
+ ~entityName,
107
+ schema->S.castToUnknown,
108
+ )
98
109
 
99
110
  testSync("a lone `*Id` field is the only thing the key could be", () => {
100
111
  let schema = S.schema(s => {"productId": s.matches(S.string), "name": s.matches(S.string)})
@@ -137,12 +148,65 @@ describe("resolveKeyField — the ladder", () => {
137
148
  })
138
149
  })
139
150
 
151
+ // A read model's row is keyed by the envelope id of the events it folds, so its
152
+ // lone `*Id` field is a foreign id: `Orders = {customerId, …}` is not keyed by the
153
+ // customer.
154
+ describe("resolveKeyField — a read model is keyed by its envelope", () => {
155
+ let keyFor = (~entityName, schema) =>
156
+ GraphQL_FragmentGenerator.resolveKeyField(
157
+ ~rowKeying=RowKeyedByEnvelope,
158
+ ~entityName,
159
+ schema->S.castToUnknown,
160
+ )
161
+
162
+ testSync("a lone foreign `*Id` field does not name the key", () => {
163
+ let schema = S.schema(
164
+ s => {"customerId": s.matches(S.string), "lifecycle": s.matches(S.string)},
165
+ )
166
+ expect(keyFor(~entityName="Orders", schema))->toEqual(None)
167
+ })
168
+
169
+ testSync("the convention rung still names it", () => {
170
+ let schema = S.schema(s => {"orderId": s.matches(S.string), "customerId": s.matches(S.string)})
171
+ expect(keyFor(~entityName="Orders", schema))->toEqual(Some(("orderId", "convention")))
172
+ })
173
+
174
+ // The key is read off the type where there is one: a field typed as the view's
175
+ // own identity keys it whatever it is called, and one typed as another
176
+ // identity never does, whatever it is called.
177
+ testSync("a field typed as the view's identity is its key", () => {
178
+ let schema = S.schema(
179
+ s => {"order": s.matches(OrderId.schema), "customerId": s.matches(CustomerId.schema)},
180
+ )
181
+ expect(keyFor(~entityName="Orders", schema))->toEqual(Some(("order", "convention")))
182
+ })
183
+
184
+ testSync("a field named for the view but typed as another identity is not", () => {
185
+ let schema = S.schema(s => {"orderId": s.matches(CustomerId.schema)})
186
+ expect(keyFor(~entityName="Orders", schema))->toEqual(None)
187
+ })
188
+
189
+ testSync("several foreign `*Id` fields are not a gap worth a warning", () => {
190
+ let schema = S.schema(
191
+ s => {"productId": s.matches(S.string), "categoryId": s.matches(S.string)},
192
+ )
193
+ expect(
194
+ GraphQL_FragmentGenerator.classifyKeyField(
195
+ ~rowKeying=RowKeyedByEnvelope,
196
+ ~entityName="ProductDemand",
197
+ schema->S.castToUnknown,
198
+ )->GraphQL_FragmentGenerator.keyFieldGapMessage,
199
+ )->toEqual(None)
200
+ })
201
+ })
202
+
140
203
  // Losing the key is invisible — the view keeps every field and every row, and
141
204
  // drops its `<field>Eq` filter and its whole `orderBy` from the SDL. These pin
142
205
  // which of the two ways to lose it is worth saying out loud.
143
206
  describe("keyFieldGapMessage — which gap is worth a warning", () => {
144
207
  let gapFor = (~entityName, schema) =>
145
208
  GraphQL_FragmentGenerator.classifyKeyField(
209
+ ~rowKeying=RowKeyedByStateField,
146
210
  ~entityName,
147
211
  schema->S.castToUnknown,
148
212
  )->GraphQL_FragmentGenerator.keyFieldGapMessage
@@ -197,6 +261,17 @@ describe("deriveServerCapability — inferred keys reach the SDL surface", () =>
197
261
  expect(c.sortFields)->toEqual(["orderId"])
198
262
  })
199
263
 
264
+ // Not the key of a read model, but filterable all the same: removing a
265
+ // generated filter is an SDL break.
266
+ testSync("a lone foreign `*Id` field stays filterable and sortable", () => {
267
+ let schema = S.schema(
268
+ s => {"customerId": s.matches(S.string), "lifecycle": s.matches(S.string)},
269
+ )
270
+ let c = capabilityFor(~entityName="Orders", schema)
271
+ expect(c.filterFields->Array.map(f => f.name))->toEqual(["customerId"])
272
+ expect(c.sortFields)->toEqual(["customerId"])
273
+ })
274
+
200
275
  testSync("an unresolvable key still yields nothing — the old behaviour, now narrowed", () => {
201
276
  let schema = S.schema(s => {"email": s.matches(S.string)})
202
277
  let c = capabilityFor(~entityName="Customers", schema)
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Sury from "sury";
4
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as Money$Reventless from "@reventlessdev/reventless-spec/src/semantic/Money.res.mjs";
6
7
  import * as GraphQL_Stitcher$ReventlessCore from "../../src/components/Api/GraphQL_Stitcher.res.mjs";
@@ -9,6 +10,14 @@ import * as GraphQL_FragmentGenerator$ReventlessCore from "../../src/components/
9
10
  import * as PsInternalFieldsReadModel$ReventlessCore from "./PsInternalFieldsReadModel.res.mjs";
10
11
  import * as OutboundTranslationSlice_Callback$ReventlessCore from "../../src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
11
12
 
13
+ let OrderId = Id$Reventless.Make({
14
+ key: "orderId"
15
+ });
16
+
17
+ let CustomerId = Id$Reventless.Make({
18
+ key: "customerId"
19
+ });
20
+
12
21
  let commandSchema = Sury.union([
13
22
  Sury.$schema(s => ({
14
23
  TAG: "CancelOrder",
@@ -100,7 +109,7 @@ globalThis.describe("resolveKeyField — the ladder", () => {
100
109
  productId: s.m(Sury.string),
101
110
  name: s.m(Sury.string)
102
111
  }));
103
- globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("AvailableProducts", schema)).toEqual([
112
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "AvailableProducts", schema)).toEqual([
104
113
  "productId",
105
114
  "sole"
106
115
  ]);
@@ -110,7 +119,7 @@ globalThis.describe("resolveKeyField — the ladder", () => {
110
119
  productId: s.m(Sury.string),
111
120
  categoryId: s.m(Sury.string)
112
121
  }));
113
- globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("Products", schema)).toEqual([
122
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "Products", schema)).toEqual([
114
123
  "productId",
115
124
  "convention"
116
125
  ]);
@@ -120,7 +129,7 @@ globalThis.describe("resolveKeyField — the ladder", () => {
120
129
  categoryId: s.m(Sury.string),
121
130
  ownerId: s.m(Sury.string)
122
131
  }));
123
- globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("Categories", schema)).toEqual([
132
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "Categories", schema)).toEqual([
124
133
  "categoryId",
125
134
  "convention"
126
135
  ]);
@@ -130,53 +139,94 @@ globalThis.describe("resolveKeyField — the ladder", () => {
130
139
  productId: s.m(Sury.string),
131
140
  categoryId: s.m(Sury.string)
132
141
  }));
133
- globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("ProductDemand", schema)).toEqual(undefined);
142
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "ProductDemand", schema)).toEqual(undefined);
134
143
  });
135
144
  globalThis.test("no `*Id` field at all declines", () => {
136
145
  let schema = Sury.$schema(s => ({
137
146
  email: s.m(Sury.string),
138
147
  address: s.m(Sury.string)
139
148
  }));
140
- globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("Customers", schema)).toEqual(undefined);
149
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "Customers", schema)).toEqual(undefined);
141
150
  });
142
151
  globalThis.test("a word merely ending in `id` is not a key", () => {
143
152
  let schema = Sury.$schema(s => ({
144
153
  paid: s.m(Sury.string),
145
154
  valid: s.m(Sury.string)
146
155
  }));
147
- globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("Invoices", schema)).toEqual(undefined);
156
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "Invoices", schema)).toEqual(undefined);
157
+ });
158
+ });
159
+
160
+ globalThis.describe("resolveKeyField — a read model is keyed by its envelope", () => {
161
+ globalThis.test("a lone foreign `*Id` field does not name the key", () => {
162
+ let schema = Sury.$schema(s => ({
163
+ customerId: s.m(Sury.string),
164
+ lifecycle: s.m(Sury.string)
165
+ }));
166
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", "Orders", schema)).toEqual(undefined);
167
+ });
168
+ globalThis.test("the convention rung still names it", () => {
169
+ let schema = Sury.$schema(s => ({
170
+ orderId: s.m(Sury.string),
171
+ customerId: s.m(Sury.string)
172
+ }));
173
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", "Orders", schema)).toEqual([
174
+ "orderId",
175
+ "convention"
176
+ ]);
177
+ });
178
+ globalThis.test("a field typed as the view's identity is its key", () => {
179
+ let schema = Sury.$schema(s => ({
180
+ order: s.m(OrderId.schema),
181
+ customerId: s.m(CustomerId.schema)
182
+ }));
183
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", "Orders", schema)).toEqual([
184
+ "order",
185
+ "convention"
186
+ ]);
187
+ });
188
+ globalThis.test("a field named for the view but typed as another identity is not", () => {
189
+ let schema = Sury.$schema(s => ({
190
+ orderId: s.m(CustomerId.schema)
191
+ }));
192
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", "Orders", schema)).toEqual(undefined);
193
+ });
194
+ globalThis.test("several foreign `*Id` fields are not a gap worth a warning", () => {
195
+ let schema = Sury.$schema(s => ({
196
+ productId: s.m(Sury.string),
197
+ categoryId: s.m(Sury.string)
198
+ }));
199
+ globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("RowKeyedByEnvelope", "ProductDemand", schema))).toEqual(undefined);
148
200
  });
149
201
  });
150
202
 
151
203
  globalThis.describe("keyFieldGapMessage — which gap is worth a warning", () => {
204
+ let gapFor = (entityName, schema) => GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("RowKeyedByStateField", entityName, schema));
152
205
  globalThis.test("a second `*Id` field taking the key away is named, with both fields", () => {
153
206
  let schema = Sury.$schema(s => ({
154
207
  productId: s.m(Sury.string),
155
208
  categoryId: s.m(Sury.string)
156
209
  }));
157
- let message = Stdlib_Option.getOr(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("ProductDemand", schema)), "");
210
+ let message = Stdlib_Option.getOr(gapFor("ProductDemand", schema), "");
158
211
  globalThis.expect(message.includes("productId, categoryId")).toBe(true);
159
212
  globalThis.expect(message.includes("productDemandId")).toBe(true);
160
213
  });
161
214
  globalThis.test("a state with no `*Id` field at all is silent, not warned about", () => {
162
- let schema = Sury.$schema(s => ({
215
+ globalThis.expect(gapFor("Customers", Sury.$schema(s => ({
163
216
  email: s.m(Sury.string),
164
217
  address: s.m(Sury.string)
165
- }));
166
- globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("Customers", schema))).toEqual(undefined);
218
+ })))).toEqual(undefined);
167
219
  });
168
220
  globalThis.test("a resolved key says nothing", () => {
169
- let schema = Sury.$schema(s => ({
221
+ globalThis.expect(gapFor("Orders", Sury.$schema(s => ({
170
222
  orderId: s.m(Sury.string)
171
- }));
172
- globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("Orders", schema))).toEqual(undefined);
223
+ })))).toEqual(undefined);
173
224
  });
174
225
  globalThis.test("an @id-less view whose name picks one of its `*Id` fields is fine", () => {
175
- let schema = Sury.$schema(s => ({
226
+ globalThis.expect(gapFor("Products", Sury.$schema(s => ({
176
227
  productId: s.m(Sury.string),
177
228
  categoryId: s.m(Sury.string)
178
- }));
179
- globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("Products", schema))).toEqual(undefined);
229
+ })))).toEqual(undefined);
180
230
  });
181
231
  });
182
232
 
@@ -190,6 +240,15 @@ globalThis.describe("deriveServerCapability — inferred keys reach the SDL surf
190
240
  globalThis.expect(c.filterFields.map(f => f.name)).toEqual(["orderId"]);
191
241
  globalThis.expect(c.sortFields).toEqual(["orderId"]);
192
242
  });
243
+ globalThis.test("a lone foreign `*Id` field stays filterable and sortable", () => {
244
+ let schema = Sury.$schema(s => ({
245
+ customerId: s.m(Sury.string),
246
+ lifecycle: s.m(Sury.string)
247
+ }));
248
+ let c = GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability("Orders", schema);
249
+ globalThis.expect(c.filterFields.map(f => f.name)).toEqual(["customerId"]);
250
+ globalThis.expect(c.sortFields).toEqual(["customerId"]);
251
+ });
193
252
  globalThis.test("an unresolvable key still yields nothing — the old behaviour, now narrowed", () => {
194
253
  let schema = Sury.$schema(s => ({
195
254
  email: s.m(Sury.string)
@@ -668,6 +727,8 @@ globalThis.describe("@resolves / @resolvesMany reach the SDL", () => {
668
727
  });
669
728
 
670
729
  export {
730
+ OrderId,
731
+ CustomerId,
671
732
  OrderSliceCmd,
672
733
  mutationFor,
673
734
  GeocodeItem,
@@ -680,4 +741,4 @@ export {
680
741
  refDoorFragment,
681
742
  ResolvedFields,
682
743
  }
683
- /* commandSchema Not a pure module */
744
+ /* OrderId Not a pure module */
@@ -0,0 +1,77 @@
1
+ // An identity-typed field is an id whatever it is called: `buyer: CustomerId.t`
2
+ // must reach the SDL as `ID!` and carry its key on the JSON Schema, or a field
3
+ // that is neither tagged nor referenced loses its id-ness on the wire.
4
+
5
+ open JestGlobals
6
+
7
+ module OrderId = Reventless.Id.Make({
8
+ let key = "orderId"
9
+ })
10
+ module CustomerId = Reventless.Id.Make({
11
+ let key = "customerId"
12
+ })
13
+
14
+ @schema
15
+ type order = {
16
+ buyer: CustomerId.t,
17
+ previous: option<OrderId.t>,
18
+ related: array<OrderId.t>,
19
+ note: string,
20
+ }
21
+
22
+ let property = (json: JSON.t, name: string): option<JSON.t> =>
23
+ json
24
+ ->JSON.Decode.object
25
+ ->Option.flatMap(o => o->Dict.get("properties"))
26
+ ->Option.flatMap(JSON.Decode.object)
27
+ ->Option.flatMap(p => p->Dict.get(name))
28
+
29
+ let key = (json: option<JSON.t>, name: string): option<JSON.t> =>
30
+ json->Option.flatMap(JSON.Decode.object)->Option.flatMap(o => o->Dict.get(name))
31
+
32
+ describe("the identity semantic on the JSON Schema", () => {
33
+ let json = SuryToJsonSchema.deriveObjectSchema(orderSchema->S.castToUnknown)
34
+
35
+ testSync("names the semantic and its key", () => {
36
+ let buyer = json->property("buyer")
37
+ expect(buyer->key("x-reventless-semantic"))->toEqual(Some(JSON.Encode.string("identity")))
38
+ expect(buyer->key("x-reventless-semantic-target"))->toEqual(
39
+ Some(JSON.Encode.object(Dict.fromArray([("key", JSON.Encode.string("customerId"))]))),
40
+ )
41
+ })
42
+
43
+ testSync("reaches an array's elements", () =>
44
+ expect(
45
+ json
46
+ ->property("related")
47
+ ->key("items")
48
+ ->key("x-reventless-semantic-target")
49
+ ->key("key"),
50
+ )->toEqual(Some(JSON.Encode.string("orderId")))
51
+ )
52
+
53
+ testSync("leaves a plain string alone", () =>
54
+ expect(json->property("note")->key("x-reventless-semantic"))->toEqual(None)
55
+ )
56
+ })
57
+
58
+ describe("the identity semantic in the SDL", () => {
59
+ let sdl =
60
+ GraphQL_FragmentGenerator.deriveObjectTypeWithNested(
61
+ ~typeName="Ordering_Order",
62
+ ~includeIdParam=false,
63
+ orderSchema->S.castToUnknown,
64
+ )->Array.join("\n")
65
+
66
+ testSync("an identity field not named `*Id` is an ID", () =>
67
+ expect(sdl->String.includes("buyer: ID!"))->toBe(true)
68
+ )
69
+
70
+ testSync("an optional identity keeps its optionality", () =>
71
+ expect(sdl->String.includes("previous: ID\n"))->toBe(true)
72
+ )
73
+
74
+ testSync("an array of identities is a list of IDs", () =>
75
+ expect(sdl->String.includes("related: [ID!]!"))->toBe(true)
76
+ )
77
+ })
@@ -0,0 +1,71 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as SuryToJsonSchema$ReventlessCore from "../../src/components/Api/SuryToJsonSchema.res.mjs";
8
+ import * as GraphQL_FragmentGenerator$ReventlessCore from "../../src/components/Api/GraphQL_FragmentGenerator.res.mjs";
9
+
10
+ let OrderId = Id$Reventless.Make({
11
+ key: "orderId"
12
+ });
13
+
14
+ let CustomerId = Id$Reventless.Make({
15
+ key: "customerId"
16
+ });
17
+
18
+ let orderSchema = Sury.$schema(s => ({
19
+ buyer: s.m(CustomerId.schema),
20
+ previous: s.m(Sury.$option(OrderId.schema)),
21
+ related: s.m(Sury.array(OrderId.schema)),
22
+ note: s.m(Sury.string)
23
+ }));
24
+
25
+ function property(json, name) {
26
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o["properties"]), Stdlib_JSON.Decode.object), p => p[name]);
27
+ }
28
+
29
+ function key(json, name) {
30
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(json, Stdlib_JSON.Decode.object), o => o[name]);
31
+ }
32
+
33
+ globalThis.describe("the identity semantic on the JSON Schema", () => {
34
+ let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, orderSchema);
35
+ globalThis.test("names the semantic and its key", () => {
36
+ let buyer = property(json, "buyer");
37
+ globalThis.expect(key(buyer, "x-reventless-semantic")).toEqual("identity");
38
+ globalThis.expect(key(buyer, "x-reventless-semantic-target")).toEqual(Object.fromEntries([[
39
+ "key",
40
+ "customerId"
41
+ ]]));
42
+ });
43
+ globalThis.test("reaches an array's elements", () => {
44
+ globalThis.expect(key(key(key(property(json, "related"), "items"), "x-reventless-semantic-target"), "key")).toEqual("orderId");
45
+ });
46
+ globalThis.test("leaves a plain string alone", () => {
47
+ globalThis.expect(key(property(json, "note"), "x-reventless-semantic")).toEqual(undefined);
48
+ });
49
+ });
50
+
51
+ globalThis.describe("the identity semantic in the SDL", () => {
52
+ let sdl = GraphQL_FragmentGenerator$ReventlessCore.deriveObjectTypeWithNested("Ordering_Order", undefined, false, undefined, orderSchema).join("\n");
53
+ globalThis.test("an identity field not named `*Id` is an ID", () => {
54
+ globalThis.expect(sdl.includes("buyer: ID!")).toBe(true);
55
+ });
56
+ globalThis.test("an optional identity keeps its optionality", () => {
57
+ globalThis.expect(sdl.includes("previous: ID\n")).toBe(true);
58
+ });
59
+ globalThis.test("an array of identities is a list of IDs", () => {
60
+ globalThis.expect(sdl.includes("related: [ID!]!")).toBe(true);
61
+ });
62
+ });
63
+
64
+ export {
65
+ OrderId,
66
+ CustomerId,
67
+ orderSchema,
68
+ property,
69
+ key,
70
+ }
71
+ /* OrderId Not a pure module */