@reventlessdev/reventless-core 3.0.0-alpha.265 → 3.0.0-alpha.267
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 +30 -0
- package/package.json +5 -5
- package/src/Projection.res +0 -10
- package/src/Projection.res.mjs +0 -14
- package/src/ProjectionMapper.res +8 -2
- package/src/ProjectionMapper.res.mjs +3 -3
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
- package/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs +3 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res +47 -16
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +20 -7
- package/src/components/Api/SchemaType.res +13 -3
- package/src/components/Api/SchemaType.res.mjs +13 -2
- package/src/components/Api/SuryToJsonSchema.res +9 -5
- package/src/components/Api/SuryToJsonSchema.res.mjs +14 -2
- package/src/components/EventMapper/EventMapper_Builder.res +19 -1
- package/src/components/EventMapper/EventMapper_Builder.res.mjs +10 -1
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +11 -1
- package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +2 -1
- package/src/components/StateViewSlice/StateViewSlice_Callback.res +8 -1
- package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +2 -1
- package/src/plugin/component/Plugin_Structure.res +148 -25
- package/src/plugin/component/Plugin_Structure.res.mjs +114 -30
- package/tests/api/GraphQL_FragmentGeneratorTest.res +76 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +78 -17
- package/tests/api/IdentitySemanticTest.res +77 -0
- package/tests/api/IdentitySemanticTest.res.mjs +71 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +0 -1
- package/tests/dcb/DcbStateChangeSliceTest.res.mjs +0 -3
- package/tests/eventmapper/EventMapperFixtures.res +2 -2
- package/tests/eventmapper/EventMapperFixtures.res.mjs +14 -2
- package/tests/plugin/IdentityReferenceTest.res +99 -0
- package/tests/plugin/IdentityReferenceTest.res.mjs +169 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +14 -1
- package/tests/plugin/PluginStructureTest.res.mjs +42 -10
- package/tests/plugin/StateView/PsAnnotatedView.res.mjs +3 -0
- package/tests/plugin/StateView/PsAvailableProductsView.res.mjs +3 -0
- package/tests/plugin/StateView/PsCategoriesView.res.mjs +3 -0
- package/tests/plugin/StateView/PsCustomersView.res.mjs +3 -0
- package/tests/plugin/StateView/PsGatedView.res.mjs +3 -0
- package/tests/plugin/StateView/PsOrdersView.res.mjs +3 -0
- package/tests/plugin/StateView/PsShipmentsView.res.mjs +3 -0
|
@@ -587,31 +587,92 @@ let labelFieldsFromStateSchema = (
|
|
|
587
587
|
// Mirrors the command walk for emitted events. Module-level so Platform_Admin,
|
|
588
588
|
// whose components never pass through `make`, derives its defs the same way.
|
|
589
589
|
|
|
590
|
+
/**
|
|
591
|
+
The plugin's views by the identity each is keyed by, which is what lets a typed
|
|
592
|
+
id find the view that lists it without an `@ref`. `report` says where the type
|
|
593
|
+
cannot decide: several views keyed by the identity, none, or an `@ref` naming a
|
|
594
|
+
view keyed by another.
|
|
595
|
+
*/
|
|
596
|
+
type identityViews = {
|
|
597
|
+
viewsByKey: dict<array<string>>,
|
|
598
|
+
viewNames: array<string>,
|
|
599
|
+
report: string => unit,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// The reference a typed id implies, and the check on one it declares.
|
|
603
|
+
let identityReference = (
|
|
604
|
+
identityViews: identityViews,
|
|
605
|
+
~fieldName,
|
|
606
|
+
fieldSchema: S.t<unknown>,
|
|
607
|
+
declared: array<(string, Reventless.Reference.target)>,
|
|
608
|
+
): array<(string, Reventless.Reference.target)> =>
|
|
609
|
+
switch Reventless.Semantic.fieldIdentityKey(fieldSchema) {
|
|
610
|
+
| None => declared
|
|
611
|
+
| Some(key) =>
|
|
612
|
+
let keyed = identityViews.viewsByKey->Dict.get(key)->Option.getOr([])
|
|
613
|
+
switch declared {
|
|
614
|
+
| [] =>
|
|
615
|
+
switch keyed {
|
|
616
|
+
| [view] => [(fieldName, {Reventless.Semantic.entity: view, plugin: None, identity: key})]
|
|
617
|
+
| [] =>
|
|
618
|
+
identityViews.report(
|
|
619
|
+
`${fieldName} is a ${key}, and no view in this plugin is keyed by it, so it references nothing. List the identity in a view, or reference another plugin's with @ref("Plugin.View").`,
|
|
620
|
+
)
|
|
621
|
+
[]
|
|
622
|
+
| several =>
|
|
623
|
+
identityViews.report(
|
|
624
|
+
`${fieldName} is a ${key}, and ${several->Array.join(
|
|
625
|
+
" and ",
|
|
626
|
+
)} are all keyed by it. Say which it references with @ref.`,
|
|
627
|
+
)
|
|
628
|
+
[]
|
|
629
|
+
}
|
|
630
|
+
| _ =>
|
|
631
|
+
declared->Array.forEach(((_, target)) =>
|
|
632
|
+
if (
|
|
633
|
+
target.plugin->Option.isNone &&
|
|
634
|
+
identityViews.viewNames->Array.includes(target.entity) &&
|
|
635
|
+
!(keyed->Array.includes(target.entity))
|
|
636
|
+
) {
|
|
637
|
+
identityViews.report(
|
|
638
|
+
`${fieldName} is a ${key}, and its @ref names ${target.entity}, which is not keyed by it.`,
|
|
639
|
+
)
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
declared
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
590
646
|
// A variant's cross-entity references, shared by the command and event walks.
|
|
591
647
|
// `getFieldTarget`, not `getTarget`: an `array<string>` field declares it on the
|
|
592
648
|
// element schema.
|
|
593
|
-
let extractReferences = (
|
|
649
|
+
let extractReferences = (
|
|
650
|
+
~identityViews: option<identityViews>=?,
|
|
651
|
+
properties: dict<S.t<unknown>>,
|
|
652
|
+
): array<Reventless.Plugin.fieldReference> =>
|
|
594
653
|
properties
|
|
595
654
|
->Dict.toArray
|
|
596
|
-
->Array.flatMap(((fieldName, fieldSchema)) =>
|
|
655
|
+
->Array.flatMap(((fieldName, fieldSchema)) => {
|
|
597
656
|
// `collectFieldTargets`, not `getFieldTarget`: a reference declared on a field
|
|
598
657
|
// of a record the field holds (an order line's `productId`) is one this
|
|
599
658
|
// command declares, and it arrives named by its own path. Commands and events
|
|
600
659
|
// both come through here, which is what stops one of them learning about
|
|
601
660
|
// nesting and the other not.
|
|
602
|
-
Reventless.Reference.collectFieldTargets(fieldName, fieldSchema)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
661
|
+
let declared = Reventless.Reference.collectFieldTargets(fieldName, fieldSchema)
|
|
662
|
+
let targets = switch identityViews {
|
|
663
|
+
| Some(views) => identityReference(views, ~fieldName, fieldSchema, declared)
|
|
664
|
+
| None => declared
|
|
665
|
+
}
|
|
666
|
+
targets->Array.map(((path, target)): Reventless.Plugin.fieldReference => {
|
|
606
667
|
Reventless.Plugin.fieldName: path,
|
|
607
668
|
entity: target.entity,
|
|
608
669
|
plugin: target.plugin,
|
|
609
670
|
})
|
|
610
|
-
)
|
|
671
|
+
})
|
|
611
672
|
|
|
612
|
-
let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
673
|
+
let toEventDef = (~identityViews=?, v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
613
674
|
let mkDef = (~variantName, ~properties) => {
|
|
614
|
-
let references = extractReferences(properties)
|
|
675
|
+
let references = extractReferences(~identityViews?, properties)
|
|
615
676
|
|
|
616
677
|
(
|
|
617
678
|
{
|
|
@@ -640,10 +701,12 @@ let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
|
640
701
|
}
|
|
641
702
|
}
|
|
642
703
|
|
|
643
|
-
let extractEventDefs = (eventSchema: S.t<unknown>): array<
|
|
704
|
+
let extractEventDefs = (~identityViews=?, eventSchema: S.t<unknown>): array<
|
|
705
|
+
Reventless.Plugin.eventDef,
|
|
706
|
+
> =>
|
|
644
707
|
switch eventSchema {
|
|
645
|
-
| AnyOf({anyOf}) => anyOf->Array.filterMap(toEventDef)
|
|
646
|
-
| _ => toEventDef(eventSchema)->Option.mapOr([], def => [def])
|
|
708
|
+
| AnyOf({anyOf}) => anyOf->Array.filterMap(v => toEventDef(~identityViews?, v))
|
|
709
|
+
| _ => toEventDef(~identityViews?, eventSchema)->Option.mapOr([], def => [def])
|
|
647
710
|
}
|
|
648
711
|
|
|
649
712
|
// Errors walk identically to events; only the def type differs, so a consumer
|
|
@@ -750,6 +813,7 @@ let annotateArgTypes = (schema: JSON.t, argTypes: dict<string>): JSON.t => {
|
|
|
750
813
|
}
|
|
751
814
|
|
|
752
815
|
let toCommandDef = (
|
|
816
|
+
~identityViews: option<identityViews>=?,
|
|
753
817
|
~isAggregate,
|
|
754
818
|
~partitionKey: option<string>=?,
|
|
755
819
|
~mutationFieldFor: string => string,
|
|
@@ -785,7 +849,7 @@ let toCommandDef = (
|
|
|
785
849
|
~variantName,
|
|
786
850
|
properties,
|
|
787
851
|
)
|
|
788
|
-
let references = extractReferences(properties)
|
|
852
|
+
let references = extractReferences(~identityViews?, properties)
|
|
789
853
|
// Evaluated against a synthetic value per constructor, the same shape the
|
|
790
854
|
// resolver builds at call time: a payload-bearing variant compiles to
|
|
791
855
|
// `{TAG, ...}`, a payload-less one to a bare string.
|
|
@@ -923,6 +987,7 @@ let toCommandDef = (
|
|
|
923
987
|
}
|
|
924
988
|
|
|
925
989
|
let extractCommandDefs = (
|
|
990
|
+
~identityViews: option<identityViews>=?,
|
|
926
991
|
~isAggregate,
|
|
927
992
|
~partitionKey: option<string>=?,
|
|
928
993
|
~mutationFieldFor: string => string,
|
|
@@ -935,6 +1000,7 @@ let extractCommandDefs = (
|
|
|
935
1000
|
| AnyOf({anyOf}) =>
|
|
936
1001
|
anyOf->Array.filterMap(v =>
|
|
937
1002
|
toCommandDef(
|
|
1003
|
+
~identityViews?,
|
|
938
1004
|
~isAggregate,
|
|
939
1005
|
~partitionKey?,
|
|
940
1006
|
~mutationFieldFor,
|
|
@@ -948,6 +1014,7 @@ let extractCommandDefs = (
|
|
|
948
1014
|
| _ =>
|
|
949
1015
|
// Single-variant command types compile to a bare Object schema, not a Union.
|
|
950
1016
|
toCommandDef(
|
|
1017
|
+
~identityViews?,
|
|
951
1018
|
~isAggregate,
|
|
952
1019
|
~partitionKey?,
|
|
953
1020
|
~mutationFieldFor,
|
|
@@ -986,9 +1053,12 @@ let queryableDefFromSpec = (
|
|
|
986
1053
|
): Reventless.Plugin.queryableDef => {
|
|
987
1054
|
let qf = Api_Naming.queryFieldNamesForReadModel(~plugin, ~name)
|
|
988
1055
|
let label = labelFieldsFromStateSchema(~entityName=name, stateSchema)
|
|
989
|
-
// The
|
|
990
|
-
|
|
991
|
-
|
|
1056
|
+
// The published key is always filterable; not every filterable field is the key.
|
|
1057
|
+
let keyField = GraphQL_FragmentGenerator.resolveKeyField(
|
|
1058
|
+
~rowKeying=RowKeyedByEnvelope,
|
|
1059
|
+
~entityName=name,
|
|
1060
|
+
stateSchema,
|
|
1061
|
+
)
|
|
992
1062
|
{
|
|
993
1063
|
Reventless.Plugin.name,
|
|
994
1064
|
queryField: qf.listFieldName,
|
|
@@ -1414,8 +1484,8 @@ let make = (
|
|
|
1414
1484
|
|
|
1415
1485
|
// Warned rather than refused: an unkeyed view still answers, just without a
|
|
1416
1486
|
// filter or an ordering, and a lint that stops a deploy gets silenced.
|
|
1417
|
-
let recordKeyField = (~entityName, stateSchema) =>
|
|
1418
|
-
GraphQL_FragmentGenerator.classifyKeyField(~entityName, stateSchema)
|
|
1487
|
+
let recordKeyField = (~rowKeying, ~entityName, stateSchema) =>
|
|
1488
|
+
GraphQL_FragmentGenerator.classifyKeyField(~rowKeying, ~entityName, stateSchema)
|
|
1419
1489
|
->GraphQL_FragmentGenerator.keyFieldGapMessage
|
|
1420
1490
|
->Option.forEach(why => log.warn(~comp="Plugin_Structure", `${name}/${entityName} ${why}`))
|
|
1421
1491
|
|
|
@@ -1425,14 +1495,17 @@ let make = (
|
|
|
1425
1495
|
let qf = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
|
|
1426
1496
|
let stateSchema = R.Spec.stateSchema->S.castToUnknown
|
|
1427
1497
|
let label = labelFieldsFromStateSchema(~entityName=R.Spec.name, stateSchema)
|
|
1428
|
-
// The
|
|
1429
|
-
|
|
1430
|
-
|
|
1498
|
+
// The published key is always filterable; not every filterable field is the key.
|
|
1499
|
+
let keyField = GraphQL_FragmentGenerator.resolveKeyField(
|
|
1500
|
+
~rowKeying=RowKeyedByEnvelope,
|
|
1501
|
+
~entityName=R.Spec.name,
|
|
1502
|
+
stateSchema,
|
|
1503
|
+
)
|
|
1431
1504
|
// Qualified to the plugin's event ids so they match the producers' nodes.
|
|
1432
1505
|
let consumed = qualify(~prefix=name, R.consumedEventNames)
|
|
1433
1506
|
recordRetired(~entityName=R.Spec.name, stateSchema)
|
|
1434
1507
|
recordLifecycle(~entityName=R.Spec.name, stateSchema)
|
|
1435
|
-
recordKeyField(~entityName=R.Spec.name, stateSchema)
|
|
1508
|
+
recordKeyField(~rowKeying=RowKeyedByEnvelope, ~entityName=R.Spec.name, stateSchema)
|
|
1436
1509
|
|
|
1437
1510
|
(
|
|
1438
1511
|
{
|
|
@@ -1471,12 +1544,13 @@ let make = (
|
|
|
1471
1544
|
let stateSchema = SVS.Spec.stateSchema->S.castToUnknown
|
|
1472
1545
|
let label = labelFieldsFromStateSchema(~entityName=SVS.Spec.name, stateSchema)
|
|
1473
1546
|
let keyField = GraphQL_FragmentGenerator.resolveKeyField(
|
|
1547
|
+
~rowKeying=RowKeyedByStateField,
|
|
1474
1548
|
~entityName=SVS.Spec.name,
|
|
1475
1549
|
stateSchema,
|
|
1476
1550
|
)
|
|
1477
1551
|
recordRetired(~entityName=SVS.Spec.name, stateSchema)
|
|
1478
1552
|
recordLifecycle(~entityName=SVS.Spec.name, stateSchema)
|
|
1479
|
-
recordKeyField(~entityName=SVS.Spec.name, stateSchema)
|
|
1553
|
+
recordKeyField(~rowKeying=RowKeyedByStateField, ~entityName=SVS.Spec.name, stateSchema)
|
|
1480
1554
|
|
|
1481
1555
|
(
|
|
1482
1556
|
{
|
|
@@ -1529,6 +1603,53 @@ let make = (
|
|
|
1529
1603
|
}
|
|
1530
1604
|
let partitionFailures = []
|
|
1531
1605
|
|
|
1606
|
+
// Each view's key as an identity: a read model's `Spec.Id` when it is one, else
|
|
1607
|
+
// the key field's type, else the key field's name, so a view keyed by an untyped
|
|
1608
|
+
// `productId` still lists the `productId` identity.
|
|
1609
|
+
let keyIdentityOf = (stateSchema: S.t<unknown>, idField: option<string>) =>
|
|
1610
|
+
idField->Option.map(field =>
|
|
1611
|
+
switch stateSchema {
|
|
1612
|
+
| Object({properties}) =>
|
|
1613
|
+
properties->Dict.get(field)->Option.flatMap(Reventless.Semantic.identityKey)
|
|
1614
|
+
| _ => None
|
|
1615
|
+
}->Option.getOr(field)
|
|
1616
|
+
)
|
|
1617
|
+
let viewKeys = Array.concat(
|
|
1618
|
+
readModels->Array.mapWithIndex((
|
|
1619
|
+
module(R: ReventlessInfra.ReadModel.T with type api = api and type role = role),
|
|
1620
|
+
i,
|
|
1621
|
+
) => {
|
|
1622
|
+
let def = readModelDefs->Array.getUnsafe(i)
|
|
1623
|
+
let key = switch R.Spec.Id.schema->Reventless.Semantic.identityKey {
|
|
1624
|
+
| Some(_) as declared => declared
|
|
1625
|
+
| None => keyIdentityOf(R.Spec.stateSchema->S.castToUnknown, def.idField)
|
|
1626
|
+
}
|
|
1627
|
+
(R.Spec.name, key)
|
|
1628
|
+
}),
|
|
1629
|
+
stateViewSlices->Array.mapWithIndex((module(SVS: ReventlessInfra.StateViewSlice.T), i) => {
|
|
1630
|
+
let def = stateViewDefs->Array.getUnsafe(i)
|
|
1631
|
+
(SVS.Spec.name, keyIdentityOf(SVS.Spec.stateSchema->S.castToUnknown, def.idField))
|
|
1632
|
+
}),
|
|
1633
|
+
)
|
|
1634
|
+
let viewsByKey: dict<array<string>> = Dict.make()
|
|
1635
|
+
viewKeys->Array.forEach(((view, key)) =>
|
|
1636
|
+
key->Option.forEach(k =>
|
|
1637
|
+
viewsByKey->Dict.set(k, viewsByKey->Dict.get(k)->Option.getOr([])->Array.concat([view]))
|
|
1638
|
+
)
|
|
1639
|
+
)
|
|
1640
|
+
// Warned rather than refused, like the key-field gap: the field still decodes,
|
|
1641
|
+
// it just links nowhere. Once per message, since a field recurs across variants.
|
|
1642
|
+
let reported: Set.t<string> = Set.make()
|
|
1643
|
+
let identityViews = {
|
|
1644
|
+
viewsByKey,
|
|
1645
|
+
viewNames: viewKeys->Array.map(((view, _)) => view),
|
|
1646
|
+
report: message =>
|
|
1647
|
+
if !(reported->Set.has(message)) {
|
|
1648
|
+
reported->Set.add(message)
|
|
1649
|
+
log.warn(~comp="Plugin_Structure", `${name}: ${message}`)
|
|
1650
|
+
},
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1532
1653
|
let stateChangeDefs =
|
|
1533
1654
|
stateChangeSlices->Array.mapWithIndex((module(SCS: ReventlessInfra.StateChangeSlice.T), i) => {
|
|
1534
1655
|
let (_, produced) = scsProduced->Array.getUnsafe(i)
|
|
@@ -1538,6 +1659,7 @@ let make = (
|
|
|
1538
1659
|
{
|
|
1539
1660
|
Reventless.Plugin.name: SCS.Spec.name,
|
|
1540
1661
|
commands: extractCommandDefs(
|
|
1662
|
+
~identityViews,
|
|
1541
1663
|
~isAggregate=false,
|
|
1542
1664
|
~partitionKey=?partitionBySlice->Dict.get(SCS.Spec.name),
|
|
1543
1665
|
~mutationFieldFor=variantName =>
|
|
@@ -1556,7 +1678,7 @@ let make = (
|
|
|
1556
1678
|
consumedEventTypes: consumed,
|
|
1557
1679
|
linkedViews: linkedSvsFor(produced),
|
|
1558
1680
|
consistencyRead: consistencyReadFor(consumed),
|
|
1559
|
-
events: extractEventDefs(SCS.Spec.eventSchema->S.castToUnknown),
|
|
1681
|
+
events: extractEventDefs(~identityViews, SCS.Spec.eventSchema->S.castToUnknown),
|
|
1560
1682
|
errors: extractErrorDefs(SCS.Spec.errorSchema->S.castToUnknown),
|
|
1561
1683
|
chapter: chapterOf(SCS.Spec.name),
|
|
1562
1684
|
}: Reventless.Plugin.writableDef
|
|
@@ -1573,6 +1695,7 @@ let make = (
|
|
|
1573
1695
|
{
|
|
1574
1696
|
Reventless.Plugin.name: A.Spec.name,
|
|
1575
1697
|
commands: extractCommandDefs(
|
|
1698
|
+
~identityViews,
|
|
1576
1699
|
~isAggregate=true,
|
|
1577
1700
|
~mutationFieldFor=variantName =>
|
|
1578
1701
|
Api_Naming.aggregateMutationField(
|
|
@@ -1589,7 +1712,7 @@ let make = (
|
|
|
1589
1712
|
consumedEventTypes: [],
|
|
1590
1713
|
linkedViews: Array.concat(linkedSvsFor(produced), linkedReadModelsFor(A.Spec.name)),
|
|
1591
1714
|
consistencyRead: None,
|
|
1592
|
-
events: extractEventDefs(A.Spec.eventSchema->S.castToUnknown),
|
|
1715
|
+
events: extractEventDefs(~identityViews, A.Spec.eventSchema->S.castToUnknown),
|
|
1593
1716
|
errors: extractErrorDefs(A.Spec.errorSchema->S.castToUnknown),
|
|
1594
1717
|
chapter: chapterOf(A.Spec.name),
|
|
1595
1718
|
}: Reventless.Plugin.writableDef
|
|
@@ -12,6 +12,7 @@ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js
|
|
|
12
12
|
import * as Trait$Reventless from "@reventlessdev/reventless-spec/src/types/Trait.res.mjs";
|
|
13
13
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
14
14
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
15
|
+
import * as Semantic$Reventless from "@reventlessdev/reventless-spec/src/semantic/Semantic.res.mjs";
|
|
15
16
|
import * as Lifecycle$Reventless from "@reventlessdev/reventless-spec/src/types/Lifecycle.res.mjs";
|
|
16
17
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
17
18
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
@@ -444,20 +445,62 @@ function labelFieldsFromStateSchema(entityName, stateSchema) {
|
|
|
444
445
|
};
|
|
445
446
|
}
|
|
446
447
|
|
|
447
|
-
function
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
448
|
+
function identityReference(identityViews, fieldName, fieldSchema, declared) {
|
|
449
|
+
let key = Semantic$Reventless.fieldIdentityKey(fieldSchema);
|
|
450
|
+
if (key === undefined) {
|
|
451
|
+
return declared;
|
|
452
|
+
}
|
|
453
|
+
let keyed = Stdlib_Option.getOr(identityViews.viewsByKey[key], []);
|
|
454
|
+
if (declared.length !== 0) {
|
|
455
|
+
declared.forEach(param => {
|
|
456
|
+
let target = param[1];
|
|
457
|
+
if (Stdlib_Option.isNone(target.plugin) && identityViews.viewNames.includes(target.entity) && !keyed.includes(target.entity)) {
|
|
458
|
+
return identityViews.report(fieldName + ` is a ` + key + `, and its @ref names ` + target.entity + `, which is not keyed by it.`);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
return declared;
|
|
462
|
+
}
|
|
463
|
+
let len = keyed.length;
|
|
464
|
+
if (len !== 1) {
|
|
465
|
+
if (len !== 0) {
|
|
466
|
+
identityViews.report(fieldName + ` is a ` + key + `, and ` + keyed.join(" and ") + ` are all keyed by it. Say which it references with @ref.`);
|
|
467
|
+
return [];
|
|
468
|
+
} else {
|
|
469
|
+
identityViews.report(fieldName + ` is a ` + key + `, and no view in this plugin is keyed by it, so it references nothing. List the identity in a view, or reference another plugin's with @ref("Plugin.View").`);
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
let view = keyed[0];
|
|
474
|
+
return [[
|
|
475
|
+
fieldName,
|
|
476
|
+
{
|
|
477
|
+
entity: view,
|
|
478
|
+
plugin: undefined,
|
|
479
|
+
identity: key
|
|
480
|
+
}
|
|
481
|
+
]];
|
|
456
482
|
}
|
|
457
483
|
|
|
458
|
-
function
|
|
484
|
+
function extractReferences(identityViews, properties) {
|
|
485
|
+
return Object.entries(properties).flatMap(param => {
|
|
486
|
+
let fieldSchema = param[1];
|
|
487
|
+
let fieldName = param[0];
|
|
488
|
+
let declared = Reference$Reventless.collectFieldTargets(fieldName, fieldSchema);
|
|
489
|
+
let targets = identityViews !== undefined ? identityReference(identityViews, fieldName, fieldSchema, declared) : declared;
|
|
490
|
+
return targets.map(param => {
|
|
491
|
+
let target = param[1];
|
|
492
|
+
return {
|
|
493
|
+
fieldName: param[0],
|
|
494
|
+
entity: target.entity,
|
|
495
|
+
plugin: target.plugin
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function toEventDef(identityViews, v) {
|
|
459
502
|
let mkDef = (variantName, properties) => {
|
|
460
|
-
let references = extractReferences(properties);
|
|
503
|
+
let references = extractReferences(identityViews, properties);
|
|
461
504
|
return {
|
|
462
505
|
name: variantName,
|
|
463
506
|
schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, v)),
|
|
@@ -488,16 +531,16 @@ function toEventDef(v) {
|
|
|
488
531
|
}
|
|
489
532
|
}
|
|
490
533
|
|
|
491
|
-
function extractEventDefs(eventSchema) {
|
|
534
|
+
function extractEventDefs(identityViews, eventSchema) {
|
|
492
535
|
if (eventSchema.type === "anyOf") {
|
|
493
|
-
return Stdlib_Array.filterMap(eventSchema.anyOf, toEventDef);
|
|
536
|
+
return Stdlib_Array.filterMap(eventSchema.anyOf, v => toEventDef(identityViews, v));
|
|
494
537
|
} else {
|
|
495
|
-
return Stdlib_Option.mapOr(toEventDef(eventSchema), [], def => [def]);
|
|
538
|
+
return Stdlib_Option.mapOr(toEventDef(identityViews, eventSchema), [], def => [def]);
|
|
496
539
|
}
|
|
497
540
|
}
|
|
498
541
|
|
|
499
542
|
function extractErrorDefs(errorSchema) {
|
|
500
|
-
return extractEventDefs(errorSchema).map(param => ({
|
|
543
|
+
return extractEventDefs(undefined, errorSchema).map(param => ({
|
|
501
544
|
name: param.name,
|
|
502
545
|
schema: param.schema,
|
|
503
546
|
references: param.references
|
|
@@ -597,10 +640,10 @@ function annotateArgTypes(schema, argTypes) {
|
|
|
597
640
|
return schema;
|
|
598
641
|
}
|
|
599
642
|
|
|
600
|
-
function toCommandDef(isAggregate, partitionKey, mutationFieldFor, parentSchema, commandAuthorization, commandTransition, derivedEdgeFor, v) {
|
|
643
|
+
function toCommandDef(identityViews, isAggregate, partitionKey, mutationFieldFor, parentSchema, commandAuthorization, commandTransition, derivedEdgeFor, v) {
|
|
601
644
|
let mkDef = (variantName, properties) => {
|
|
602
645
|
let match = commandLevelAndId(isAggregate, partitionKey, variantName, properties);
|
|
603
|
-
let references = extractReferences(properties);
|
|
646
|
+
let references = extractReferences(identityViews, properties);
|
|
604
647
|
let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
|
|
605
648
|
TAG: variantName
|
|
606
649
|
}) : variantName;
|
|
@@ -683,12 +726,12 @@ function toCommandDef(isAggregate, partitionKey, mutationFieldFor, parentSchema,
|
|
|
683
726
|
}
|
|
684
727
|
}
|
|
685
728
|
|
|
686
|
-
function extractCommandDefs(isAggregate, partitionKey, mutationFieldFor, commandAuthorization, commandTransition, derivedEdgeForOpt, commandSchema) {
|
|
729
|
+
function extractCommandDefs(identityViews, isAggregate, partitionKey, mutationFieldFor, commandAuthorization, commandTransition, derivedEdgeForOpt, commandSchema) {
|
|
687
730
|
let derivedEdgeFor = derivedEdgeForOpt !== undefined ? derivedEdgeForOpt : param => {};
|
|
688
731
|
if (commandSchema.type === "anyOf") {
|
|
689
|
-
return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, partitionKey, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, v));
|
|
732
|
+
return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(identityViews, isAggregate, partitionKey, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, v));
|
|
690
733
|
} else {
|
|
691
|
-
return Stdlib_Option.mapOr(toCommandDef(isAggregate, partitionKey, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, commandSchema), [], def => [def]);
|
|
734
|
+
return Stdlib_Option.mapOr(toCommandDef(identityViews, isAggregate, partitionKey, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, commandSchema), [], def => [def]);
|
|
692
735
|
}
|
|
693
736
|
}
|
|
694
737
|
|
|
@@ -706,7 +749,7 @@ function queryableDefFromSpec(plugin, name, stateSchema, authorization, visibili
|
|
|
706
749
|
let linkedWriteSide = linkedWriteSideOpt !== undefined ? linkedWriteSideOpt : [];
|
|
707
750
|
let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(plugin, name, undefined);
|
|
708
751
|
let label = labelFieldsFromStateSchema(name, stateSchema);
|
|
709
|
-
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(name, stateSchema);
|
|
752
|
+
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", name, stateSchema);
|
|
710
753
|
return {
|
|
711
754
|
name: name,
|
|
712
755
|
queryField: qf.listFieldName,
|
|
@@ -1006,16 +1049,16 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1006
1049
|
retiredFailures.push(f);
|
|
1007
1050
|
});
|
|
1008
1051
|
};
|
|
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));
|
|
1052
|
+
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
1053
|
let readModelDefs = readModels.map(R => {
|
|
1011
1054
|
let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(name, R.Spec.name, undefined);
|
|
1012
1055
|
let stateSchema = R.Spec.stateSchema;
|
|
1013
1056
|
let label = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
|
|
1014
|
-
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(R.Spec.name, stateSchema);
|
|
1057
|
+
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", R.Spec.name, stateSchema);
|
|
1015
1058
|
let consumed = qualify(name, R.consumedEventNames);
|
|
1016
1059
|
recordRetired(R.Spec.name, stateSchema);
|
|
1017
1060
|
recordLifecycle(R.Spec.name, stateSchema);
|
|
1018
|
-
recordKeyField(R.Spec.name, stateSchema);
|
|
1061
|
+
recordKeyField("RowKeyedByEnvelope", R.Spec.name, stateSchema);
|
|
1019
1062
|
return {
|
|
1020
1063
|
name: R.Spec.name,
|
|
1021
1064
|
queryField: qf.listFieldName,
|
|
@@ -1044,10 +1087,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1044
1087
|
let consumed = match[1];
|
|
1045
1088
|
let stateSchema = SVS.Spec.stateSchema;
|
|
1046
1089
|
let label = labelFieldsFromStateSchema(SVS.Spec.name, stateSchema);
|
|
1047
|
-
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(SVS.Spec.name, stateSchema);
|
|
1090
|
+
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", SVS.Spec.name, stateSchema);
|
|
1048
1091
|
recordRetired(SVS.Spec.name, stateSchema);
|
|
1049
1092
|
recordLifecycle(SVS.Spec.name, stateSchema);
|
|
1050
|
-
recordKeyField(SVS.Spec.name, stateSchema);
|
|
1093
|
+
recordKeyField("RowKeyedByStateField", SVS.Spec.name, stateSchema);
|
|
1051
1094
|
return {
|
|
1052
1095
|
name: SVS.Spec.name,
|
|
1053
1096
|
queryField: qf.listFieldName,
|
|
@@ -1101,6 +1144,46 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1101
1144
|
};
|
|
1102
1145
|
}
|
|
1103
1146
|
let partitionFailures = [];
|
|
1147
|
+
let keyIdentityOf = (stateSchema, idField) => Stdlib_Option.map(idField, field => {
|
|
1148
|
+
let tmp;
|
|
1149
|
+
tmp = stateSchema.type === "object" ? Stdlib_Option.flatMap(stateSchema.properties[field], Semantic$Reventless.identityKey) : undefined;
|
|
1150
|
+
return Stdlib_Option.getOr(tmp, field);
|
|
1151
|
+
});
|
|
1152
|
+
let viewKeys = readModels.map((R, i) => {
|
|
1153
|
+
let def = readModelDefs[i];
|
|
1154
|
+
let declared = Semantic$Reventless.identityKey(R.Spec.Id.schema);
|
|
1155
|
+
let key = declared !== undefined ? declared : keyIdentityOf(R.Spec.stateSchema, def.idField);
|
|
1156
|
+
return [
|
|
1157
|
+
R.Spec.name,
|
|
1158
|
+
key
|
|
1159
|
+
];
|
|
1160
|
+
}).concat(stateViewSlices.map((SVS, i) => {
|
|
1161
|
+
let def = stateViewDefs[i];
|
|
1162
|
+
return [
|
|
1163
|
+
SVS.Spec.name,
|
|
1164
|
+
keyIdentityOf(SVS.Spec.stateSchema, def.idField)
|
|
1165
|
+
];
|
|
1166
|
+
}));
|
|
1167
|
+
let viewsByKey = {};
|
|
1168
|
+
viewKeys.forEach(param => {
|
|
1169
|
+
let view = param[0];
|
|
1170
|
+
Stdlib_Option.forEach(param[1], k => {
|
|
1171
|
+
viewsByKey[k] = Stdlib_Option.getOr(viewsByKey[k], []).concat([view]);
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
let reported = new Set();
|
|
1175
|
+
let identityViews_viewNames = viewKeys.map(param => param[0]);
|
|
1176
|
+
let identityViews_report = message => {
|
|
1177
|
+
if (!reported.has(message)) {
|
|
1178
|
+
reported.add(message);
|
|
1179
|
+
return log.warn("Plugin_Structure", undefined, name + `: ` + message);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
let identityViews = {
|
|
1183
|
+
viewsByKey: viewsByKey,
|
|
1184
|
+
viewNames: identityViews_viewNames,
|
|
1185
|
+
report: identityViews_report
|
|
1186
|
+
};
|
|
1104
1187
|
let stateChangeDefs = stateChangeSlices.map((SCS, i) => {
|
|
1105
1188
|
let match = scsProduced[i];
|
|
1106
1189
|
let produced = match[1];
|
|
@@ -1108,12 +1191,12 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1108
1191
|
let consumed = match$1[1];
|
|
1109
1192
|
return {
|
|
1110
1193
|
name: SCS.Spec.name,
|
|
1111
|
-
commands: extractCommandDefs(false, partitionBySlice[SCS.Spec.name], variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandTransition, extra => derivedEdgeFor(SCS.Spec.name, extra), SCS.Spec.commandSchema),
|
|
1194
|
+
commands: extractCommandDefs(identityViews, false, partitionBySlice[SCS.Spec.name], variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandTransition, extra => derivedEdgeFor(SCS.Spec.name, extra), SCS.Spec.commandSchema),
|
|
1112
1195
|
producedEventTypes: produced,
|
|
1113
1196
|
consumedEventTypes: consumed,
|
|
1114
1197
|
linkedViews: linkedSvsFor(produced),
|
|
1115
1198
|
consistencyRead: consistencyReadFor(consumed),
|
|
1116
|
-
events: extractEventDefs(SCS.Spec.eventSchema),
|
|
1199
|
+
events: extractEventDefs(identityViews, SCS.Spec.eventSchema),
|
|
1117
1200
|
errors: extractErrorDefs(SCS.Spec.errorSchema),
|
|
1118
1201
|
chapter: componentChapters[SCS.Spec.name]
|
|
1119
1202
|
};
|
|
@@ -1123,12 +1206,12 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1123
1206
|
let produced = match[1];
|
|
1124
1207
|
return {
|
|
1125
1208
|
name: A.Spec.name,
|
|
1126
|
-
commands: extractCommandDefs(true, undefined, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, extra => derivedEdgeFor(A.Spec.name, extra), A.Spec.commandSchema),
|
|
1209
|
+
commands: extractCommandDefs(identityViews, true, undefined, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, extra => derivedEdgeFor(A.Spec.name, extra), A.Spec.commandSchema),
|
|
1127
1210
|
producedEventTypes: produced,
|
|
1128
1211
|
consumedEventTypes: [],
|
|
1129
1212
|
linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
|
|
1130
1213
|
consistencyRead: undefined,
|
|
1131
|
-
events: extractEventDefs(A.Spec.eventSchema),
|
|
1214
|
+
events: extractEventDefs(identityViews, A.Spec.eventSchema),
|
|
1132
1215
|
errors: extractErrorDefs(A.Spec.errorSchema),
|
|
1133
1216
|
chapter: componentChapters[A.Spec.name]
|
|
1134
1217
|
};
|
|
@@ -1452,6 +1535,7 @@ export {
|
|
|
1452
1535
|
checkLifecycleTopology,
|
|
1453
1536
|
labelFieldSourceToString,
|
|
1454
1537
|
labelFieldsFromStateSchema,
|
|
1538
|
+
identityReference,
|
|
1455
1539
|
extractReferences,
|
|
1456
1540
|
toEventDef,
|
|
1457
1541
|
extractEventDefs,
|
|
@@ -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(
|
|
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)
|