@reventlessdev/reventless-spec 3.0.0-alpha.129 → 3.0.0-alpha.130
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 +16 -0
- package/package.json +1 -1
- package/schema/platform-api.graphql +1 -0
- package/src/components/DcbTag.res +229 -32
- package/src/components/DcbTag.res.mjs +232 -41
- package/src/components/Plugin.res +47 -4
- package/src/components/Plugin.res.mjs +1 -0
- package/src/components/Reference.res +48 -0
- package/src/components/Reference.res.mjs +28 -0
- package/src/generator/Codegen.res +14 -1
- package/src/generator/Codegen.res.mjs +9 -5
- package/src/generator/PluginGenerator.res +4 -1
- package/src/generator/PluginGenerator.res.mjs +2 -1
- package/src/types/Transition.res +30 -3
- package/src/types/Transition.res.mjs +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.130 (2026-09-07)
|
|
7
|
+
|
|
8
|
+
* feat(dcb)!: a record inside an array is still a reference and a key ([bcb9b13](https://github.com/ReventlessDev/reventless-core/commit/bcb9b13958323d8319d7fcf25c055800e84150f3))
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **core:** a command's lifecycle edge comes from its scenarios, not its claim ([1879d05](https://github.com/ReventlessDev/reventless-core/commit/1879d0570a981e7fae5bcd3808f8a36e898bd0fd))
|
|
12
|
+
|
|
13
|
+
### BREAKING CHANGES
|
|
14
|
+
|
|
15
|
+
* an `int` field is now `Int` in the generated SDL and
|
|
16
|
+
`"integer"` in JSON Schema, where it was `Float`/`"number"`. A client
|
|
17
|
+
declaring `$retryCount: Float!` against one of these fields must move to
|
|
18
|
+
`Int!`.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# 3.0.0-alpha.129 (2026-09-04)
|
|
7
23
|
|
|
8
24
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -273,6 +273,56 @@ let isTaggedArray = (fieldSchema: S.t<unknown>) =>
|
|
|
273
273
|
| _ => false
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
The properties of the record a field's value holds, when it holds records rather
|
|
278
|
+
than scalars — `record`, `option<record>`, `array<record>`, `option<array<record>>`
|
|
279
|
+
— with `true` for the array forms, which fan out into one contribution per element.
|
|
280
|
+
|
|
281
|
+
This is the single wrapper walk the nested-marker walks share, so a marker means
|
|
282
|
+
the same thing whichever of them finds it. It follows exactly the wrappers a
|
|
283
|
+
field's own value can wear and stops at the first record: a marker two records
|
|
284
|
+
deep is a shape no domain here has, and reaching it is a different and much more
|
|
285
|
+
expensive walk than this one.
|
|
286
|
+
*/
|
|
287
|
+
let nestedRecordProperties = (fieldSchema: S.t<unknown>): option<(dict<S.t<unknown>>, bool)> => {
|
|
288
|
+
let unwrapped = fieldSchema->Semantic.unwrapOptional->Option.getOr(fieldSchema)
|
|
289
|
+
let (candidate, isList) = switch unwrapped {
|
|
290
|
+
| Array({additionalItems: Schema(item)}) => (
|
|
291
|
+
item->Semantic.unwrapOptional->Option.getOr(item),
|
|
292
|
+
true,
|
|
293
|
+
)
|
|
294
|
+
| other => (other, false)
|
|
295
|
+
}
|
|
296
|
+
switch candidate {
|
|
297
|
+
| Object({properties}) => Some((properties, isList))
|
|
298
|
+
| _ => None
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Returns `true` if a record's own properties carry at least one DCB tag. */
|
|
303
|
+
let propertiesCarryTags = (properties: dict<S.t<unknown>>): bool =>
|
|
304
|
+
properties->Dict.toArray->Array.some(((_, s)) => isTagged(s) || isTaggedArray(s))
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
Returns `true` if the schema is an array of records at least one of whose fields
|
|
308
|
+
is DCB-tagged — `array<lineItem>` where `lineItem` carries a `@ref` or a `*Id`.
|
|
309
|
+
|
|
310
|
+
Distinguished from [`hasNestedTags`] because an array is the case that *fans out*:
|
|
311
|
+
it decides the query mode, exactly as a tagged scalar array does.
|
|
312
|
+
*/
|
|
313
|
+
let isTaggedRecordArray = (fieldSchema: S.t<unknown>): bool =>
|
|
314
|
+
switch nestedRecordProperties(fieldSchema) {
|
|
315
|
+
| Some((properties, true)) => propertiesCarryTags(properties)
|
|
316
|
+
| _ => false
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Returns `true` if the schema holds a record — in an array or not — carrying tags. */
|
|
320
|
+
let hasNestedTags = (fieldSchema: S.t<unknown>): bool =>
|
|
321
|
+
switch nestedRecordProperties(fieldSchema) {
|
|
322
|
+
| Some((properties, _)) => propertiesCarryTags(properties)
|
|
323
|
+
| None => false
|
|
324
|
+
}
|
|
325
|
+
|
|
276
326
|
/** Returns `true` if the schema was annotated with `DcbTag.partition`. */
|
|
277
327
|
let isPartitionTag = (fieldSchema: S.t<unknown>) =>
|
|
278
328
|
S.Metadata.get(fieldSchema, ~id=dcbPartitionTagId)->Option.isSome
|
|
@@ -318,25 +368,127 @@ let jsonValueToString = json =>
|
|
|
318
368
|
| _ => json->JSON.stringify
|
|
319
369
|
}
|
|
320
370
|
|
|
371
|
+
/**
|
|
372
|
+
Collapses repeated `(key, value)` pairs, keeping the first of each.
|
|
373
|
+
|
|
374
|
+
Two order lines for the same product carry the same tag twice. A flat field could
|
|
375
|
+
never produce that, which is why nothing downstream guards against it: a repeated
|
|
376
|
+
clause in a decision query and a repeated entry in a write index are both created
|
|
377
|
+
here, so both are collapsed here. The key is JSON-quoted before the value is
|
|
378
|
+
appended, so the boundary between the two is unambiguous.
|
|
379
|
+
*/
|
|
380
|
+
let dedupeTags = (tags: array<tag>): array<tag> => {
|
|
381
|
+
let seen = Set.make()
|
|
382
|
+
tags->Array.filter(({key, value}) => {
|
|
383
|
+
let identity = key->JSON.Encode.string->JSON.stringify ++ value
|
|
384
|
+
if seen->Set.has(identity) {
|
|
385
|
+
false
|
|
386
|
+
} else {
|
|
387
|
+
seen->Set.add(identity)
|
|
388
|
+
true
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
The tags one object's *own* properties carry: every scalar tagged field, and one
|
|
395
|
+
tag per element of every tagged scalar array.
|
|
396
|
+
|
|
397
|
+
Shared by the top-level expanded walk and by the nested-record descent below, so
|
|
398
|
+
a marker means the same thing at either depth.
|
|
399
|
+
*/
|
|
400
|
+
let flatFieldTagsExpanded = (
|
|
401
|
+
fieldName: string,
|
|
402
|
+
fieldSchema: S.t<unknown>,
|
|
403
|
+
jsonDict: dict<JSON.t>,
|
|
404
|
+
): array<tag> =>
|
|
405
|
+
if isTagged(fieldSchema) {
|
|
406
|
+
switch jsonDict->Dict.get(fieldName) {
|
|
407
|
+
| Some(jsonValue) => [
|
|
408
|
+
{key: resolveTagKey(fieldName, fieldSchema), value: jsonValue->jsonValueToString},
|
|
409
|
+
]
|
|
410
|
+
| None => []
|
|
411
|
+
}
|
|
412
|
+
} else if isTaggedArray(fieldSchema) {
|
|
413
|
+
switch jsonDict->Dict.get(fieldName) {
|
|
414
|
+
| Some(JSON.Array(elements)) =>
|
|
415
|
+
let tagKey = resolveArrayTagKey(fieldName, fieldSchema)
|
|
416
|
+
elements->Array.map(element => {key: tagKey, value: element->jsonValueToString})
|
|
417
|
+
| _ => []
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
[]
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let flatTagsExpanded = (properties: dict<S.t<unknown>>, jsonDict: dict<JSON.t>): array<tag> =>
|
|
424
|
+
properties
|
|
425
|
+
->Dict.toArray
|
|
426
|
+
->Array.flatMap(((fieldName, fieldSchema)) =>
|
|
427
|
+
flatFieldTagsExpanded(fieldName, fieldSchema, jsonDict)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
The tags carried by the records a field holds — one contribution per element for
|
|
432
|
+
an array, one for a plain nested record.
|
|
433
|
+
|
|
434
|
+
**The key comes from the nested field's name, never the enclosing one.**
|
|
435
|
+
`lineItems[].productId` produces `productId`, which is what makes it the *same*
|
|
436
|
+
tag the catalog's own events write. A key derived from `lineItems` would name a
|
|
437
|
+
tag no producer has ever written, and the decision read would come back empty —
|
|
438
|
+
the identical symptom to extracting no tags at all, from a different cause. An
|
|
439
|
+
author who wants another key still writes `DcbTag.stringForKey` on the nested
|
|
440
|
+
field, which `resolveTagKey` honours here as it does at the top level.
|
|
441
|
+
*/
|
|
442
|
+
let nestedRecordTags = (fieldSchema: S.t<unknown>, jsonValue: JSON.t): array<tag> =>
|
|
443
|
+
switch nestedRecordProperties(fieldSchema) {
|
|
444
|
+
| None => []
|
|
445
|
+
| Some((properties, isList)) =>
|
|
446
|
+
let ofObject = json =>
|
|
447
|
+
switch json->JSON.Decode.object {
|
|
448
|
+
| Some(elementDict) => flatTagsExpanded(properties, elementDict)
|
|
449
|
+
| None => []
|
|
450
|
+
}
|
|
451
|
+
if isList {
|
|
452
|
+
switch jsonValue {
|
|
453
|
+
| JSON.Array(elements) => elements->Array.flatMap(ofObject)
|
|
454
|
+
| _ => []
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
ofObject(jsonValue)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
321
461
|
/**
|
|
322
462
|
Extracts tags from a flat JSON object given a map of field schemas.
|
|
323
463
|
Only fields whose schema is tagged (via `DcbTag.string` / `DcbTag.int`) are extracted.
|
|
464
|
+
|
|
465
|
+
A field holding *records* is descended into, and this is the one case where this
|
|
466
|
+
walk and [`extractTagsFromPropertiesExpanded`] agree: they differ on a scalar
|
|
467
|
+
array (one stringified tag here, one tag per element there) because a stringified
|
|
468
|
+
array is at least a value, whereas a stringified line-item list is nothing any
|
|
469
|
+
producer could match. A reader must not generalise the difference.
|
|
324
470
|
*/
|
|
325
471
|
let extractTagsFromProperties = (properties: dict<S.t<unknown>>, jsonDict: dict<JSON.t>) =>
|
|
326
472
|
properties
|
|
327
473
|
->Dict.toArray
|
|
328
|
-
->Array.
|
|
474
|
+
->Array.flatMap(((fieldName, fieldSchema)) =>
|
|
329
475
|
if isTagged(fieldSchema) {
|
|
330
|
-
jsonDict
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
476
|
+
switch jsonDict->Dict.get(fieldName) {
|
|
477
|
+
| Some(jsonValue) => [
|
|
478
|
+
{key: resolveTagKey(fieldName, fieldSchema), value: jsonValue->jsonValueToString},
|
|
479
|
+
]
|
|
480
|
+
| None => []
|
|
481
|
+
}
|
|
482
|
+
} else if hasNestedTags(fieldSchema) {
|
|
483
|
+
switch jsonDict->Dict.get(fieldName) {
|
|
484
|
+
| Some(jsonValue) => nestedRecordTags(fieldSchema, jsonValue)
|
|
485
|
+
| None => []
|
|
486
|
+
}
|
|
336
487
|
} else {
|
|
337
|
-
|
|
488
|
+
[]
|
|
338
489
|
}
|
|
339
490
|
)
|
|
491
|
+
->dedupeTags
|
|
340
492
|
|
|
341
493
|
// Extract the discriminating TAG constructor name from a variant Object schema's
|
|
342
494
|
// `properties`. sury (11) compiles a record-payload variant `Foo({...})` to an
|
|
@@ -517,6 +669,10 @@ For scalar tagged fields, behaves identically to `extractTagsFromProperties`.
|
|
|
517
669
|
For array tagged fields, produces one tag per element. Tag keys honour an optional
|
|
518
670
|
`DcbTag.stringForKey(~key=...)` override on the (inner) schema; otherwise the field
|
|
519
671
|
name is used.
|
|
672
|
+
|
|
673
|
+
Fields holding records are descended into on the same terms as in
|
|
674
|
+
`extractTagsFromProperties` — the two walks agree there and differ only on scalar
|
|
675
|
+
arrays; see that function.
|
|
520
676
|
*/
|
|
521
677
|
let extractTagsFromPropertiesExpanded = (
|
|
522
678
|
properties: dict<S.t<unknown>>,
|
|
@@ -525,25 +681,18 @@ let extractTagsFromPropertiesExpanded = (
|
|
|
525
681
|
properties
|
|
526
682
|
->Dict.toArray
|
|
527
683
|
->Array.flatMap(((fieldName, fieldSchema)) =>
|
|
528
|
-
if isTagged(fieldSchema) {
|
|
684
|
+
if isTagged(fieldSchema) || isTaggedArray(fieldSchema) {
|
|
685
|
+
flatFieldTagsExpanded(fieldName, fieldSchema, jsonDict)
|
|
686
|
+
} else if hasNestedTags(fieldSchema) {
|
|
529
687
|
switch jsonDict->Dict.get(fieldName) {
|
|
530
|
-
| Some(jsonValue) =>
|
|
531
|
-
{key: resolveTagKey(fieldName, fieldSchema), value: jsonValue->jsonValueToString},
|
|
532
|
-
]
|
|
688
|
+
| Some(jsonValue) => nestedRecordTags(fieldSchema, jsonValue)
|
|
533
689
|
| None => []
|
|
534
690
|
}
|
|
535
|
-
} else if isTaggedArray(fieldSchema) {
|
|
536
|
-
switch jsonDict->Dict.get(fieldName) {
|
|
537
|
-
| Some(JSON.Array(elements)) => {
|
|
538
|
-
let tagKey = resolveArrayTagKey(fieldName, fieldSchema)
|
|
539
|
-
elements->Array.map(element => {key: tagKey, value: element->jsonValueToString})
|
|
540
|
-
}
|
|
541
|
-
| _ => []
|
|
542
|
-
}
|
|
543
691
|
} else {
|
|
544
692
|
[]
|
|
545
693
|
}
|
|
546
694
|
)
|
|
695
|
+
->dedupeTags
|
|
547
696
|
|
|
548
697
|
/**
|
|
549
698
|
Extracts DCB tags from an event JSON value, expanding array values into per-element tags.
|
|
@@ -605,8 +754,9 @@ let extractTagsExpanded = (schema: S.t<'a>, value: 'a): array<tag> =>
|
|
|
605
754
|
// --- Automatic query construction from command schema ---
|
|
606
755
|
|
|
607
756
|
/**
|
|
608
|
-
Returns `true` if any field in the schema
|
|
609
|
-
(`array<@s.matches(DcbTag.string) string>`)
|
|
757
|
+
Returns `true` if any field in the schema fans out into several tags — a tagged
|
|
758
|
+
scalar array (`array<@s.matches(DcbTag.string) string>`) or an array of records
|
|
759
|
+
carrying tags (`array<lineItem>`).
|
|
610
760
|
Used to automatically determine whether to build single-clause or multi-clause queries.
|
|
611
761
|
*/
|
|
612
762
|
let hasTaggedArrayFields = (schema: S.t<'a>): bool =>
|
|
@@ -615,21 +765,30 @@ let hasTaggedArrayFields = (schema: S.t<'a>): bool =>
|
|
|
615
765
|
anyOf->Array.some(variantSchema =>
|
|
616
766
|
switch variantSchema {
|
|
617
767
|
| Object({properties}) =>
|
|
618
|
-
properties
|
|
768
|
+
properties
|
|
769
|
+
->Dict.toArray
|
|
770
|
+
->Array.some(((_, fieldSchema)) =>
|
|
771
|
+
isTaggedArray(fieldSchema) || isTaggedRecordArray(fieldSchema)
|
|
772
|
+
)
|
|
619
773
|
| _ => false
|
|
620
774
|
}
|
|
621
775
|
)
|
|
622
776
|
| Object({properties}) =>
|
|
623
|
-
properties
|
|
777
|
+
properties
|
|
778
|
+
->Dict.toArray
|
|
779
|
+
->Array.some(((_, fieldSchema)) =>
|
|
780
|
+
isTaggedArray(fieldSchema) || isTaggedRecordArray(fieldSchema)
|
|
781
|
+
)
|
|
624
782
|
| _ => false
|
|
625
783
|
}
|
|
626
784
|
|
|
627
785
|
/**
|
|
628
786
|
Collects the produced tag keys of one event-schema variant: the resolved tag key
|
|
629
|
-
of every scalar tagged field
|
|
630
|
-
tagged-array field
|
|
787
|
+
of every scalar tagged field, the resolved (override-aware) tag key of every
|
|
788
|
+
tagged-array field, and the keys of the tagged fields of any records the variant
|
|
789
|
+
holds — under the nested field's own name, as the extraction resolves them.
|
|
631
790
|
*/
|
|
632
|
-
let
|
|
791
|
+
let flatTagKeysOfProperties = (properties: dict<S.t<unknown>>): array<string> =>
|
|
633
792
|
properties
|
|
634
793
|
->Dict.toArray
|
|
635
794
|
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
@@ -642,6 +801,22 @@ let tagKeysOfProperties = (properties: dict<S.t<unknown>>): array<string> =>
|
|
|
642
801
|
}
|
|
643
802
|
)
|
|
644
803
|
|
|
804
|
+
let tagKeysOfProperties = (properties: dict<S.t<unknown>>): array<string> =>
|
|
805
|
+
properties
|
|
806
|
+
->Dict.toArray
|
|
807
|
+
->Array.flatMap(((fieldName, fieldSchema)) =>
|
|
808
|
+
if isTagged(fieldSchema) {
|
|
809
|
+
[resolveTagKey(fieldName, fieldSchema)]
|
|
810
|
+
} else if isTaggedArray(fieldSchema) {
|
|
811
|
+
[resolveArrayTagKey(fieldName, fieldSchema)]
|
|
812
|
+
} else {
|
|
813
|
+
switch nestedRecordProperties(fieldSchema) {
|
|
814
|
+
| Some((nested, _)) => flatTagKeysOfProperties(nested)
|
|
815
|
+
| None => []
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
)
|
|
819
|
+
|
|
645
820
|
/**
|
|
646
821
|
Maps each event-type (variant constructor name) to the set of DCB tag keys that
|
|
647
822
|
type can carry, read from a *produced* event-log schema.
|
|
@@ -882,21 +1057,43 @@ let extractCrossPartitionTagKeys = (schema: S.t<'event>): array<string> => {
|
|
|
882
1057
|
Collects the `*Id` / `*Ids`-shaped fields of one object-variant's properties as
|
|
883
1058
|
`DcbScopeInference.idField`s — by **name**, independent of any DCB tag flag. This
|
|
884
1059
|
is the un-annotated structural view the scope inference consumes.
|
|
1060
|
+
|
|
1061
|
+
A field holding records contributes the `*Id`-shaped fields of those records
|
|
1062
|
+
under their **own** names, and a field reached through an array is reported as a
|
|
1063
|
+
list whichever container it came from: the inference reasons about keys, and a
|
|
1064
|
+
key reached per line fans out exactly as a key reached per array element does.
|
|
1065
|
+
Reporting it as single-valued would derive the wrong partition for the slice.
|
|
1066
|
+
|
|
1067
|
+
That `DcbScopeInference.idField` needs no change to carry this is the evidence
|
|
1068
|
+
the boundary was drawn in the right place — the descent is a fact about schemas,
|
|
1069
|
+
which is this module's half of the split.
|
|
885
1070
|
*/
|
|
886
|
-
let idFieldsOfProperties = (properties: dict<S.t<unknown>>): array<DcbScopeInference.idField> =>
|
|
1071
|
+
let idFieldsOfProperties = (properties: dict<S.t<unknown>>): array<DcbScopeInference.idField> => {
|
|
1072
|
+
let isIdName = (name: string) => name->String.endsWith("Ids") || name->String.endsWith("Id")
|
|
887
1073
|
properties
|
|
888
1074
|
->Dict.toArray
|
|
889
|
-
->Array.
|
|
890
|
-
if
|
|
1075
|
+
->Array.flatMap(((name, fieldSchema)) =>
|
|
1076
|
+
if isIdName(name) {
|
|
891
1077
|
let isList = switch fieldSchema {
|
|
892
1078
|
| Array(_) => true
|
|
893
1079
|
| _ => false
|
|
894
1080
|
}
|
|
895
|
-
|
|
1081
|
+
[{DcbScopeInference.name, isList}]
|
|
896
1082
|
} else {
|
|
897
|
-
|
|
1083
|
+
switch nestedRecordProperties(fieldSchema) {
|
|
1084
|
+
| Some((nested, nestedIsList)) =>
|
|
1085
|
+
nested
|
|
1086
|
+
->Dict.toArray
|
|
1087
|
+
->Array.filterMap(((nestedName, _)) =>
|
|
1088
|
+
isIdName(nestedName)
|
|
1089
|
+
? Some({DcbScopeInference.name: nestedName, isList: nestedIsList})
|
|
1090
|
+
: None
|
|
1091
|
+
)
|
|
1092
|
+
| None => []
|
|
1093
|
+
}
|
|
898
1094
|
}
|
|
899
1095
|
)
|
|
1096
|
+
}
|
|
900
1097
|
|
|
901
1098
|
/**
|
|
902
1099
|
Extracts the `DcbScopeInference.eventShape`s (variant name + `*Id` fields) from a
|
|
@@ -8,6 +8,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
8
8
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
9
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
10
10
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
11
|
+
import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
|
|
11
12
|
import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
|
|
12
13
|
import * as DcbScopeInference$Reventless from "./DcbScopeInference.res.mjs";
|
|
13
14
|
|
|
@@ -86,6 +87,62 @@ function isTaggedArray(fieldSchema) {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
function nestedRecordProperties(fieldSchema) {
|
|
91
|
+
let unwrapped = Stdlib_Option.getOr(Semantic$Reventless.unwrapOptional(fieldSchema), fieldSchema);
|
|
92
|
+
let match;
|
|
93
|
+
if (unwrapped.type === "array") {
|
|
94
|
+
let item = unwrapped.additionalItems;
|
|
95
|
+
match = item === "strip" || item === "strict" ? [
|
|
96
|
+
unwrapped,
|
|
97
|
+
false
|
|
98
|
+
] : [
|
|
99
|
+
Stdlib_Option.getOr(Semantic$Reventless.unwrapOptional(item), item),
|
|
100
|
+
true
|
|
101
|
+
];
|
|
102
|
+
} else {
|
|
103
|
+
match = [
|
|
104
|
+
unwrapped,
|
|
105
|
+
false
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
let candidate = match[0];
|
|
109
|
+
if (candidate.type === "object") {
|
|
110
|
+
return [
|
|
111
|
+
candidate.properties,
|
|
112
|
+
match[1]
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function propertiesCarryTags(properties) {
|
|
118
|
+
return Object.entries(properties).some(param => {
|
|
119
|
+
let s = param[1];
|
|
120
|
+
if (Stdlib_Option.isSome(Sury.$Metadata_get(s, dcbTagId))) {
|
|
121
|
+
return true;
|
|
122
|
+
} else {
|
|
123
|
+
return isTaggedArray(s);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isTaggedRecordArray(fieldSchema) {
|
|
129
|
+
let match = nestedRecordProperties(fieldSchema);
|
|
130
|
+
if (match !== undefined && match[1]) {
|
|
131
|
+
return propertiesCarryTags(match[0]);
|
|
132
|
+
} else {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasNestedTags(fieldSchema) {
|
|
138
|
+
let match = nestedRecordProperties(fieldSchema);
|
|
139
|
+
if (match !== undefined) {
|
|
140
|
+
return propertiesCarryTags(match[0]);
|
|
141
|
+
} else {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
89
146
|
function isPartitionTag(fieldSchema) {
|
|
90
147
|
return Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbPartitionTagId));
|
|
91
148
|
}
|
|
@@ -139,17 +196,102 @@ function jsonValueToString(json) {
|
|
|
139
196
|
}
|
|
140
197
|
}
|
|
141
198
|
|
|
199
|
+
function dedupeTags(tags) {
|
|
200
|
+
let seen = new Set();
|
|
201
|
+
return tags.filter(param => {
|
|
202
|
+
let identity = JSON.stringify(param.key) + param.value;
|
|
203
|
+
if (seen.has(identity)) {
|
|
204
|
+
return false;
|
|
205
|
+
} else {
|
|
206
|
+
seen.add(identity);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function flatFieldTagsExpanded(fieldName, fieldSchema, jsonDict) {
|
|
213
|
+
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId))) {
|
|
214
|
+
let jsonValue = jsonDict[fieldName];
|
|
215
|
+
if (jsonValue !== undefined) {
|
|
216
|
+
return [{
|
|
217
|
+
key: resolveTagKey(fieldName, fieldSchema),
|
|
218
|
+
value: jsonValueToString(jsonValue)
|
|
219
|
+
}];
|
|
220
|
+
} else {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!isTaggedArray(fieldSchema)) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
let match = jsonDict[fieldName];
|
|
228
|
+
if (match === undefined) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
if (!Array.isArray(match)) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
let tagKey = resolveArrayTagKey(fieldName, fieldSchema);
|
|
235
|
+
return match.map(element => ({
|
|
236
|
+
key: tagKey,
|
|
237
|
+
value: jsonValueToString(element)
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function flatTagsExpanded(properties, jsonDict) {
|
|
242
|
+
return Object.entries(properties).flatMap(param => flatFieldTagsExpanded(param[0], param[1], jsonDict));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function nestedRecordTags(fieldSchema, jsonValue) {
|
|
246
|
+
let match = nestedRecordProperties(fieldSchema);
|
|
247
|
+
if (match === undefined) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
let properties = match[0];
|
|
251
|
+
let ofObject = json => {
|
|
252
|
+
let elementDict = Stdlib_JSON.Decode.object(json);
|
|
253
|
+
if (elementDict !== undefined) {
|
|
254
|
+
return flatTagsExpanded(properties, elementDict);
|
|
255
|
+
} else {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
if (match[1]) {
|
|
260
|
+
if (Array.isArray(jsonValue)) {
|
|
261
|
+
return jsonValue.flatMap(ofObject);
|
|
262
|
+
} else {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
return ofObject(jsonValue);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
142
270
|
function extractTagsFromProperties(properties, jsonDict) {
|
|
143
|
-
return
|
|
271
|
+
return dedupeTags(Object.entries(properties).flatMap(param => {
|
|
144
272
|
let fieldSchema = param[1];
|
|
145
273
|
let fieldName = param[0];
|
|
146
274
|
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId))) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
275
|
+
let jsonValue = jsonDict[fieldName];
|
|
276
|
+
if (jsonValue !== undefined) {
|
|
277
|
+
return [{
|
|
278
|
+
key: resolveTagKey(fieldName, fieldSchema),
|
|
279
|
+
value: jsonValueToString(jsonValue)
|
|
280
|
+
}];
|
|
281
|
+
} else {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
151
284
|
}
|
|
152
|
-
|
|
285
|
+
if (!hasNestedTags(fieldSchema)) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
let jsonValue$1 = jsonDict[fieldName];
|
|
289
|
+
if (jsonValue$1 !== undefined) {
|
|
290
|
+
return nestedRecordTags(fieldSchema, jsonValue$1);
|
|
291
|
+
} else {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}));
|
|
153
295
|
}
|
|
154
296
|
|
|
155
297
|
function variantTagName(properties) {
|
|
@@ -275,36 +417,22 @@ function isVariantPayloadBearing(schema, name) {
|
|
|
275
417
|
}
|
|
276
418
|
|
|
277
419
|
function extractTagsFromPropertiesExpanded(properties, jsonDict) {
|
|
278
|
-
return Object.entries(properties).flatMap(param => {
|
|
420
|
+
return dedupeTags(Object.entries(properties).flatMap(param => {
|
|
279
421
|
let fieldSchema = param[1];
|
|
280
422
|
let fieldName = param[0];
|
|
281
|
-
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId))) {
|
|
282
|
-
|
|
283
|
-
if (jsonValue !== undefined) {
|
|
284
|
-
return [{
|
|
285
|
-
key: resolveTagKey(fieldName, fieldSchema),
|
|
286
|
-
value: jsonValueToString(jsonValue)
|
|
287
|
-
}];
|
|
288
|
-
} else {
|
|
289
|
-
return [];
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (!isTaggedArray(fieldSchema)) {
|
|
293
|
-
return [];
|
|
423
|
+
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId)) || isTaggedArray(fieldSchema)) {
|
|
424
|
+
return flatFieldTagsExpanded(fieldName, fieldSchema, jsonDict);
|
|
294
425
|
}
|
|
295
|
-
|
|
296
|
-
if (match === undefined) {
|
|
426
|
+
if (!hasNestedTags(fieldSchema)) {
|
|
297
427
|
return [];
|
|
298
428
|
}
|
|
299
|
-
|
|
429
|
+
let jsonValue = jsonDict[fieldName];
|
|
430
|
+
if (jsonValue !== undefined) {
|
|
431
|
+
return nestedRecordTags(fieldSchema, jsonValue);
|
|
432
|
+
} else {
|
|
300
433
|
return [];
|
|
301
434
|
}
|
|
302
|
-
|
|
303
|
-
return match.map(element => ({
|
|
304
|
-
key: tagKey,
|
|
305
|
-
value: jsonValueToString(element)
|
|
306
|
-
}));
|
|
307
|
-
});
|
|
435
|
+
}));
|
|
308
436
|
}
|
|
309
437
|
|
|
310
438
|
function extractTagsFromJsonExpanded(schema, json) {
|
|
@@ -352,11 +480,25 @@ function extractTagsExpanded(schema, value) {
|
|
|
352
480
|
function hasTaggedArrayFields(schema) {
|
|
353
481
|
switch (schema.type) {
|
|
354
482
|
case "object" :
|
|
355
|
-
return Object.entries(schema.properties).some(param =>
|
|
483
|
+
return Object.entries(schema.properties).some(param => {
|
|
484
|
+
let fieldSchema = param[1];
|
|
485
|
+
if (isTaggedArray(fieldSchema)) {
|
|
486
|
+
return true;
|
|
487
|
+
} else {
|
|
488
|
+
return isTaggedRecordArray(fieldSchema);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
356
491
|
case "anyOf" :
|
|
357
492
|
return schema.anyOf.some(variantSchema => {
|
|
358
493
|
if (variantSchema.type === "object") {
|
|
359
|
-
return Object.entries(variantSchema.properties).some(param =>
|
|
494
|
+
return Object.entries(variantSchema.properties).some(param => {
|
|
495
|
+
let fieldSchema = param[1];
|
|
496
|
+
if (isTaggedArray(fieldSchema)) {
|
|
497
|
+
return true;
|
|
498
|
+
} else {
|
|
499
|
+
return isTaggedRecordArray(fieldSchema);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
360
502
|
} else {
|
|
361
503
|
return false;
|
|
362
504
|
}
|
|
@@ -366,7 +508,7 @@ function hasTaggedArrayFields(schema) {
|
|
|
366
508
|
}
|
|
367
509
|
}
|
|
368
510
|
|
|
369
|
-
function
|
|
511
|
+
function flatTagKeysOfProperties(properties) {
|
|
370
512
|
return Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
371
513
|
let fieldSchema = param[1];
|
|
372
514
|
let fieldName = param[0];
|
|
@@ -380,6 +522,25 @@ function tagKeysOfProperties(properties) {
|
|
|
380
522
|
});
|
|
381
523
|
}
|
|
382
524
|
|
|
525
|
+
function tagKeysOfProperties(properties) {
|
|
526
|
+
return Object.entries(properties).flatMap(param => {
|
|
527
|
+
let fieldSchema = param[1];
|
|
528
|
+
let fieldName = param[0];
|
|
529
|
+
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId))) {
|
|
530
|
+
return [resolveTagKey(fieldName, fieldSchema)];
|
|
531
|
+
}
|
|
532
|
+
if (isTaggedArray(fieldSchema)) {
|
|
533
|
+
return [resolveArrayTagKey(fieldName, fieldSchema)];
|
|
534
|
+
}
|
|
535
|
+
let match = nestedRecordProperties(fieldSchema);
|
|
536
|
+
if (match !== undefined) {
|
|
537
|
+
return flatTagKeysOfProperties(match[0]);
|
|
538
|
+
} else {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
383
544
|
function extractTagKeysByEventType(schema) {
|
|
384
545
|
let result = {};
|
|
385
546
|
let addVariant = variantSchema => {
|
|
@@ -530,17 +691,38 @@ function extractCrossPartitionTagKeys(schema) {
|
|
|
530
691
|
}
|
|
531
692
|
|
|
532
693
|
function idFieldsOfProperties(properties) {
|
|
533
|
-
|
|
694
|
+
let isIdName = name => {
|
|
695
|
+
if (name.endsWith("Ids")) {
|
|
696
|
+
return true;
|
|
697
|
+
} else {
|
|
698
|
+
return name.endsWith("Id");
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
return Object.entries(properties).flatMap(param => {
|
|
702
|
+
let fieldSchema = param[1];
|
|
534
703
|
let name = param[0];
|
|
535
|
-
if (
|
|
536
|
-
|
|
704
|
+
if (isIdName(name)) {
|
|
705
|
+
let isList;
|
|
706
|
+
isList = fieldSchema.type === "array";
|
|
707
|
+
return [{
|
|
708
|
+
name: name,
|
|
709
|
+
isList: isList
|
|
710
|
+
}];
|
|
537
711
|
}
|
|
538
|
-
let
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
712
|
+
let match = nestedRecordProperties(fieldSchema);
|
|
713
|
+
if (match === undefined) {
|
|
714
|
+
return [];
|
|
715
|
+
}
|
|
716
|
+
let nestedIsList = match[1];
|
|
717
|
+
return Stdlib_Array.filterMap(Object.entries(match[0]), param => {
|
|
718
|
+
let nestedName = param[0];
|
|
719
|
+
if (isIdName(nestedName)) {
|
|
720
|
+
return {
|
|
721
|
+
name: nestedName,
|
|
722
|
+
isList: nestedIsList
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
});
|
|
544
726
|
});
|
|
545
727
|
}
|
|
546
728
|
|
|
@@ -862,12 +1044,20 @@ export {
|
|
|
862
1044
|
derivedPartitionTagSchema,
|
|
863
1045
|
isTagged,
|
|
864
1046
|
isTaggedArray,
|
|
1047
|
+
nestedRecordProperties,
|
|
1048
|
+
propertiesCarryTags,
|
|
1049
|
+
isTaggedRecordArray,
|
|
1050
|
+
hasNestedTags,
|
|
865
1051
|
isPartitionTag,
|
|
866
1052
|
isCrossPartitionTag,
|
|
867
1053
|
isCrossPartitionTaggedArray,
|
|
868
1054
|
resolveTagKey,
|
|
869
1055
|
resolveArrayTagKey,
|
|
870
1056
|
jsonValueToString,
|
|
1057
|
+
dedupeTags,
|
|
1058
|
+
flatFieldTagsExpanded,
|
|
1059
|
+
flatTagsExpanded,
|
|
1060
|
+
nestedRecordTags,
|
|
871
1061
|
extractTagsFromProperties,
|
|
872
1062
|
variantTagName,
|
|
873
1063
|
extractTagsFromJson,
|
|
@@ -879,6 +1069,7 @@ export {
|
|
|
879
1069
|
extractTagsFromJsonExpanded,
|
|
880
1070
|
extractTagsExpanded,
|
|
881
1071
|
hasTaggedArrayFields,
|
|
1072
|
+
flatTagKeysOfProperties,
|
|
882
1073
|
tagKeysOfProperties,
|
|
883
1074
|
extractTagKeysByEventType,
|
|
884
1075
|
mergeTagKeysByEventType,
|
|
@@ -122,6 +122,32 @@ let uiFragmentManifestOptionSchema = S.option(uiFragmentManifestSchema)
|
|
|
122
122
|
@schema
|
|
123
123
|
type commandLevel = Collection | Instance
|
|
124
124
|
|
|
125
|
+
/** One command's lifecycle edge as the component's own scenarios describe it:
|
|
126
|
+
where a scenario shows the command taking effect, where those scenarios land,
|
|
127
|
+
and whether it brings a row into existence or acts on one.
|
|
128
|
+
|
|
129
|
+
Harvested from the GWT corpus by `check:lifecycle` and committed beside the
|
|
130
|
+
plugin as `src/LifecycleModel.res`, so structure assembly reads a value rather
|
|
131
|
+
than the test tree — which is not published with a plugin package.
|
|
132
|
+
|
|
133
|
+
No `@schema`: nothing serialises this. It is read once, while the structure is
|
|
134
|
+
assembled, and what leaves is the `commandDef` fields it resolved. */
|
|
135
|
+
type derivedEdge = {
|
|
136
|
+
/** The writable's `Spec.name`, which is what the structure knows it by. */
|
|
137
|
+
component: string,
|
|
138
|
+
command: string,
|
|
139
|
+
/** Absent where the corpus could not label a history — the honest answer when
|
|
140
|
+
the linked views declare no lifecycle field, and the reason the name-stem
|
|
141
|
+
guess is still there to fall back to. */
|
|
142
|
+
level?: commandLevel,
|
|
143
|
+
/** States a scenario shows the command taking effect from. Empty means the
|
|
144
|
+
corpus said nothing, NOT that the command belongs nowhere. */
|
|
145
|
+
allowedStates: array<string>,
|
|
146
|
+
/** States those scenarios land in. More than one is an edge a single
|
|
147
|
+
`targetState` cannot carry, so it is left to the declaration. */
|
|
148
|
+
targets: array<string>,
|
|
149
|
+
}
|
|
150
|
+
|
|
125
151
|
@schema
|
|
126
152
|
type fieldReference = {
|
|
127
153
|
fieldName: string,
|
|
@@ -137,12 +163,29 @@ type commandDef = {
|
|
|
137
163
|
aggregateIdField: option<string>,
|
|
138
164
|
mutationField: string,
|
|
139
165
|
references: array<fieldReference>,
|
|
140
|
-
/** The
|
|
141
|
-
|
|
166
|
+
/** The *from* set — lifecycle states this command is meaningful in. `None`
|
|
167
|
+
means always available; `Some([])` means never show. */
|
|
142
168
|
allowedStates: option<array<string>>,
|
|
143
|
-
/** The
|
|
144
|
-
|
|
169
|
+
/** The *to* state this command's handler writes. `None` with a from-set present
|
|
170
|
+
means the command does not move the row. */
|
|
145
171
|
targetState: option<string>,
|
|
172
|
+
/** Where the from-set came from, ranking an inherited edge against an authored
|
|
173
|
+
one the way `queryableDef.labelFieldSource` does for its field:
|
|
174
|
+
|
|
175
|
+
- `"derived"` — the component's own scenarios.
|
|
176
|
+
- `"declared"` — its `commandTransition` switch.
|
|
177
|
+
- `"unrestricted"` — the switch declares the command legal in *every*
|
|
178
|
+
state, so there is no from-set to publish and `allowedStates` is absent.
|
|
179
|
+
|
|
180
|
+
Absent means the command names no states to come from and nothing said that
|
|
181
|
+
was deliberate — a spec that wrote no switch, or one whose command creates
|
|
182
|
+
the row. So a consumer drawing a state machine can tell an edge left
|
|
183
|
+
unconstrained on purpose from one that simply has no from-set.
|
|
184
|
+
|
|
185
|
+
It speaks for the from-set only. The two halves of an edge are reported
|
|
186
|
+
separately because they fail separately: a corpus routinely shows a command
|
|
187
|
+
taking effect without ever showing where it lands. */
|
|
188
|
+
allowedStatesSource?: string,
|
|
146
189
|
/** Whether the variant is exposed in the generated API (non-`@noApi`). */
|
|
147
190
|
apiExposed: option<bool>,
|
|
148
191
|
/** Access keys — any one of them — a caller needs to be *offered* this command.
|
|
@@ -99,6 +99,7 @@ let commandDefSchema = Sury.$schema(s => ({
|
|
|
99
99
|
references: s.m(Sury.array(fieldReferenceSchema)),
|
|
100
100
|
allowedStates: s.m(Sury.$option(Sury.array(Sury.string))),
|
|
101
101
|
targetState: s.m(Sury.$option(Sury.string)),
|
|
102
|
+
allowedStatesSource: s.m(Sury.$option(Sury.string)),
|
|
102
103
|
apiExposed: s.m(Sury.$option(Sury.bool)),
|
|
103
104
|
requiredAccess: s.m(Sury.$option(Sury.array(Sury.string))),
|
|
104
105
|
ownerField: s.m(Sury.$option(Sury.string))
|
|
@@ -62,6 +62,11 @@ Only wrappers around the field's own value are followed — the optional union a
|
|
|
62
62
|
the array element, to any depth. Object properties are not: a reference declared
|
|
63
63
|
on a nested record's field belongs to that field, and attributing it to the
|
|
64
64
|
enclosing one would name the wrong field.
|
|
65
|
+
|
|
66
|
+
That is still the right answer to the question this asks. A caller collecting
|
|
67
|
+
*every* reference a command or event declares — including the ones on the fields
|
|
68
|
+
of records it holds — wants [`collectFieldTargets`], which names each one by its
|
|
69
|
+
own path.
|
|
65
70
|
*/
|
|
66
71
|
let rec getFieldTarget = (schema: S.t<unknown>): option<target> =>
|
|
67
72
|
switch getTarget(schema) {
|
|
@@ -75,6 +80,49 @@ let rec getFieldTarget = (schema: S.t<unknown>): option<target> =>
|
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
/**
|
|
84
|
+
Every reference a field declares, each paired with the path that names it.
|
|
85
|
+
|
|
86
|
+
A reference on the field's own value keeps the bare field name (`customerId`), so
|
|
87
|
+
nothing that reads today's paths has to learn the new form to go on working. One
|
|
88
|
+
declared on a field of a record the field holds is named by its path:
|
|
89
|
+
`lineItems[].productId` — `[]` for "each element of", `.` for "property of", and
|
|
90
|
+
no index, because a marker lives on the element *schema* and so applies to every
|
|
91
|
+
element or to none.
|
|
92
|
+
|
|
93
|
+
⚠️ A path is a **widened** contract, not a compatible one, for a consumer that
|
|
94
|
+
uses the name as a dictionary key into a value's properties: such a consumer
|
|
95
|
+
simply misses a nested reference and falls back to whatever it does for an
|
|
96
|
+
unreferenced field. That degradation is the correct one, but it is a degradation.
|
|
97
|
+
|
|
98
|
+
Only one record deep, matching [`DcbTag.nestedRecordProperties`], which is the
|
|
99
|
+
walk shared with the tag extraction so a marker is found in the same places by
|
|
100
|
+
both.
|
|
101
|
+
*/
|
|
102
|
+
let collectFieldTargets = (
|
|
103
|
+
fieldName: string,
|
|
104
|
+
schema: S.t<unknown>,
|
|
105
|
+
): array<(string, target)> =>
|
|
106
|
+
switch getFieldTarget(schema) {
|
|
107
|
+
| Some(target) => [(fieldName, target)]
|
|
108
|
+
| None =>
|
|
109
|
+
switch DcbTag.nestedRecordProperties(schema) {
|
|
110
|
+
| None => []
|
|
111
|
+
| Some((properties, isList)) =>
|
|
112
|
+
let prefix = isList ? fieldName ++ "[]" : fieldName
|
|
113
|
+
properties
|
|
114
|
+
->Dict.toArray
|
|
115
|
+
->Array.filterMap(((propName, propSchema)) =>
|
|
116
|
+
propName === "TAG"
|
|
117
|
+
? None
|
|
118
|
+
: getFieldTarget(propSchema)->Option.map(target => (
|
|
119
|
+
`${prefix}.${propName}`,
|
|
120
|
+
target,
|
|
121
|
+
))
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
78
126
|
/**
|
|
79
127
|
Like `to_` but does not imply DCB tag semantics.
|
|
80
128
|
Use with `@ref("Entity") @noDcbTag` when the field references another entity
|
|
@@ -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 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 "./DcbTag.res.mjs";
|
|
6
7
|
import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
|
|
@@ -53,6 +54,32 @@ function getFieldTarget(_schema) {
|
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
function collectFieldTargets(fieldName, schema) {
|
|
58
|
+
let target = getFieldTarget(schema);
|
|
59
|
+
if (target !== undefined) {
|
|
60
|
+
return [[
|
|
61
|
+
fieldName,
|
|
62
|
+
target
|
|
63
|
+
]];
|
|
64
|
+
}
|
|
65
|
+
let match = DcbTag$Reventless.nestedRecordProperties(schema);
|
|
66
|
+
if (match === undefined) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
let prefix = match[1] ? fieldName + "[]" : fieldName;
|
|
70
|
+
return Stdlib_Array.filterMap(Object.entries(match[0]), param => {
|
|
71
|
+
let propName = param[0];
|
|
72
|
+
if (propName === "TAG") {
|
|
73
|
+
return;
|
|
74
|
+
} else {
|
|
75
|
+
return Stdlib_Option.map(getFieldTarget(param[1]), target => [
|
|
76
|
+
prefix + `.` + propName,
|
|
77
|
+
target
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
56
83
|
function toWithoutDcbTag(plugin, entity) {
|
|
57
84
|
return Semantic$Reventless.mark(Sury.string, Semantic$Reventless.Id.reference, {
|
|
58
85
|
TAG: "ReferenceTo",
|
|
@@ -67,6 +94,7 @@ export {
|
|
|
67
94
|
to_,
|
|
68
95
|
getTarget,
|
|
69
96
|
getFieldTarget,
|
|
97
|
+
collectFieldTargets,
|
|
70
98
|
toWithoutDcbTag,
|
|
71
99
|
}
|
|
72
100
|
/* sury Not a pure module */
|
|
@@ -350,6 +350,7 @@ let renderPluginStructureCall = (
|
|
|
350
350
|
~extensions: array<string>,
|
|
351
351
|
~extensionPoints: array<Pairing.extensionPointDef>,
|
|
352
352
|
~componentChapters: array<(string, string)>,
|
|
353
|
+
~hasLifecycleModel: bool,
|
|
353
354
|
): option<array<string>> => {
|
|
354
355
|
// The structure call carries the EP *mapping* files (one per Delegate
|
|
355
356
|
// connection), not the wrapped ExtensionPoint module — Plugin_Structure reads
|
|
@@ -428,6 +429,15 @@ let renderPluginStructureCall = (
|
|
|
428
429
|
)
|
|
429
430
|
ls->Array.push(" ~componentChapters=Dict.fromArray([" ++ entries->Array.join(", ") ++ "]),")
|
|
430
431
|
}
|
|
432
|
+
// What the plugin's own scenarios say about each command's lifecycle edge.
|
|
433
|
+
// Written by `check:lifecycle:update`, not here: the sidecars it harvests are
|
|
434
|
+
// produced by the build this generator runs *before*, so a generator-written
|
|
435
|
+
// model would always be one build stale and a cold clone would have none at
|
|
436
|
+
// all. Referenced only when the file is on disk, so a plugin that has never
|
|
437
|
+
// been harvested keeps a byte-identical generated Plugin.res.
|
|
438
|
+
if hasLifecycleModel {
|
|
439
|
+
ls->Array.push(" ~lifecycleModel=LifecycleModel.model,")
|
|
440
|
+
}
|
|
431
441
|
ls->Array.push(" )")
|
|
432
442
|
Some(ls)
|
|
433
443
|
}
|
|
@@ -595,6 +605,7 @@ let renderComposition = (
|
|
|
595
605
|
~config: Config.config,
|
|
596
606
|
~resolved: Pairing.resolved,
|
|
597
607
|
~componentChapters: array<(string, string)>,
|
|
608
|
+
~hasLifecycleModel: bool,
|
|
598
609
|
): string => {
|
|
599
610
|
let lines: array<string> = []
|
|
600
611
|
|
|
@@ -740,6 +751,7 @@ let renderComposition = (
|
|
|
740
751
|
~extensions=resolved.extensions,
|
|
741
752
|
~extensionPoints=resolved.extensionPoints,
|
|
742
753
|
~componentChapters,
|
|
754
|
+
~hasLifecycleModel,
|
|
743
755
|
)
|
|
744
756
|
let hasPluginStructure = pluginStructureLines->Option.isSome
|
|
745
757
|
switch pluginStructureLines {
|
|
@@ -833,6 +845,7 @@ let render = (
|
|
|
833
845
|
~config: Config.config,
|
|
834
846
|
~resolved: Pairing.resolved,
|
|
835
847
|
~discovered: array<Discovery.discoveredFile>,
|
|
848
|
+
~hasLifecycleModel: bool=false,
|
|
836
849
|
): string => {
|
|
837
850
|
validateUniqueSpecStems(~discovered)
|
|
838
851
|
validateSliceTargets(~resolved)
|
|
@@ -858,6 +871,6 @@ let render = (
|
|
|
858
871
|
)
|
|
859
872
|
switch config.variant {
|
|
860
873
|
| Aws({compositionNamespace}) => renderAwsWrapper(~compositionNamespace)
|
|
861
|
-
| Composition => renderComposition(~config, ~resolved, ~componentChapters)
|
|
874
|
+
| Composition => renderComposition(~config, ~resolved, ~componentChapters, ~hasLifecycleModel)
|
|
862
875
|
}
|
|
863
876
|
}
|
|
@@ -239,7 +239,7 @@ function renderTaskMakeParam(tasks) {
|
|
|
239
239
|
return " ~tasks=[" + entries.join(", ") + "],";
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
function renderPluginStructureCall(name, aggregates, readModels, stateViewSlices, stateViewSlicesStream, stateChangeSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, extensions, extensionPoints, componentChapters) {
|
|
242
|
+
function renderPluginStructureCall(name, aggregates, readModels, stateViewSlices, stateViewSlicesStream, stateChangeSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, extensions, extensionPoints, componentChapters, hasLifecycleModel) {
|
|
243
243
|
let epMappingStems = extensionPoints.flatMap(param => param.mappings);
|
|
244
244
|
let hasComponents = aggregates.length !== 0 || readModels.length !== 0 || stateViewSlices.length !== 0 || stateViewSlicesStream.length !== 0 || stateChangeSlices.length !== 0 || automationSlices.length !== 0 || outboundTranslationSlices.length !== 0 || inboundTranslationSlices.length !== 0 || extensions.length !== 0 || epMappingStems.length !== 0;
|
|
245
245
|
if (!hasComponents) {
|
|
@@ -295,6 +295,9 @@ function renderPluginStructureCall(name, aggregates, readModels, stateViewSlices
|
|
|
295
295
|
let entries$8 = componentChapters.map(param => "(\"" + param[0] + "\", \"" + param[1] + "\")");
|
|
296
296
|
ls.push(" ~componentChapters=Dict.fromArray([" + entries$8.join(", ") + "]),");
|
|
297
297
|
}
|
|
298
|
+
if (hasLifecycleModel) {
|
|
299
|
+
ls.push(" ~lifecycleModel=LifecycleModel.model,");
|
|
300
|
+
}
|
|
298
301
|
ls.push(" )");
|
|
299
302
|
return ls;
|
|
300
303
|
}
|
|
@@ -394,7 +397,7 @@ function renderAwsWrapper(compositionNamespace) {
|
|
|
394
397
|
].join("\n");
|
|
395
398
|
}
|
|
396
399
|
|
|
397
|
-
function renderComposition(config, resolved, componentChapters) {
|
|
400
|
+
function renderComposition(config, resolved, componentChapters, hasLifecycleModel) {
|
|
398
401
|
let lines = [];
|
|
399
402
|
lines.push("// AUTO-GENERATED — do not edit. Run `npm run generate` to update.");
|
|
400
403
|
lines.push("");
|
|
@@ -459,7 +462,7 @@ function renderComposition(config, resolved, componentChapters) {
|
|
|
459
462
|
lines.push(" // Extensions");
|
|
460
463
|
push(renderExtensions(resolved.extensions));
|
|
461
464
|
}
|
|
462
|
-
let pluginStructureLines = renderPluginStructureCall(config.name, resolved.aggregates, resolved.readModels, resolved.stateViewSlices, resolved.stateViewSlicesStream, resolved.stateChangeSlices, resolved.automationSlices, resolved.outboundTranslationSlices, resolved.inboundTranslationSlices, resolved.extensions, resolved.extensionPoints, componentChapters);
|
|
465
|
+
let pluginStructureLines = renderPluginStructureCall(config.name, resolved.aggregates, resolved.readModels, resolved.stateViewSlices, resolved.stateViewSlicesStream, resolved.stateChangeSlices, resolved.automationSlices, resolved.outboundTranslationSlices, resolved.inboundTranslationSlices, resolved.extensions, resolved.extensionPoints, componentChapters, hasLifecycleModel);
|
|
463
466
|
let hasPluginStructure = Stdlib_Option.isSome(pluginStructureLines);
|
|
464
467
|
if (pluginStructureLines !== undefined) {
|
|
465
468
|
pluginStructureLines.forEach(l => {
|
|
@@ -517,7 +520,8 @@ function renderComposition(config, resolved, componentChapters) {
|
|
|
517
520
|
return lines.join("\n");
|
|
518
521
|
}
|
|
519
522
|
|
|
520
|
-
function render(config, resolved, discovered) {
|
|
523
|
+
function render(config, resolved, discovered, hasLifecycleModelOpt) {
|
|
524
|
+
let hasLifecycleModel = hasLifecycleModelOpt !== undefined ? hasLifecycleModelOpt : false;
|
|
521
525
|
validateUniqueSpecStems(discovered);
|
|
522
526
|
validateSliceTargets(resolved);
|
|
523
527
|
let componentStems = [
|
|
@@ -533,7 +537,7 @@ function render(config, resolved, discovered) {
|
|
|
533
537
|
let componentChapters = Discovery$Reventless.chaptersByStem(discovered).filter(param => componentStems.includes(param[0]));
|
|
534
538
|
let match = config.variant;
|
|
535
539
|
if (typeof match !== "object") {
|
|
536
|
-
return renderComposition(config, resolved, componentChapters);
|
|
540
|
+
return renderComposition(config, resolved, componentChapters, hasLifecycleModel);
|
|
537
541
|
} else {
|
|
538
542
|
return renderAwsWrapper(match.compositionNamespace);
|
|
539
543
|
}
|
|
@@ -34,7 +34,10 @@ let () = {
|
|
|
34
34
|
let config = {...Config.read(~srcDir), variant}
|
|
35
35
|
let discovered = Discovery.scan(~srcDir, ~exclude=config.exclude)
|
|
36
36
|
let resolved = Pairing.resolve(discovered, ~srcDir)
|
|
37
|
-
|
|
37
|
+
// Sits at the src root rather than in a kind folder, so `Discovery` never
|
|
38
|
+
// sees it and it is not a component. Its presence is the whole question.
|
|
39
|
+
let hasLifecycleModel = NodePath.join([srcDir, "LifecycleModel.res"])->NodeFs.existsSync
|
|
40
|
+
let source = Codegen.render(~config, ~resolved, ~discovered, ~hasLifecycleModel)
|
|
38
41
|
|
|
39
42
|
let outputDir = switch variant {
|
|
40
43
|
| Config.Composition => srcDir
|
|
@@ -56,7 +56,8 @@ if (srcDirArg === "") {
|
|
|
56
56
|
};
|
|
57
57
|
let discovered = Discovery$Reventless.scan(srcDir, config_exclude);
|
|
58
58
|
let resolved = Pairing$Reventless.resolve(discovered, srcDir);
|
|
59
|
-
let
|
|
59
|
+
let hasLifecycleModel = Nodefs.existsSync(Nodepath.join(srcDir, "LifecycleModel.res"));
|
|
60
|
+
let source = Codegen$Reventless.render(config, resolved, discovered, hasLifecycleModel);
|
|
60
61
|
let outputDir;
|
|
61
62
|
outputDir = typeof variant !== "object" ? srcDir : Nodepath.join(process.cwd(), "src");
|
|
62
63
|
let pluginPath = Nodepath.join(outputDir, "Plugin.res");
|
package/src/types/Transition.res
CHANGED
|
@@ -36,10 +36,23 @@
|
|
|
36
36
|
// plugin structure is assembled, and what leaves is the pair of names the
|
|
37
37
|
// structure already carried.
|
|
38
38
|
type t<'state> =
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
/** Legal in every state, and moves the row nowhere. The honest answer for a
|
|
40
|
+
report a slice publishes, which must not be refused because the row moved
|
|
41
|
+
on while the report was in flight. A claim, not an omission — see
|
|
42
|
+
`Undeclared`, which is the omission. */
|
|
42
43
|
| Unrestricted
|
|
44
|
+
/** The spec wrote no switch, and the ppx injected this. Never write it: say
|
|
45
|
+
`Unrestricted` if you mean the command is legal everywhere.
|
|
46
|
+
|
|
47
|
+
It reads exactly like `Unrestricted` — no from-set, no target — everywhere
|
|
48
|
+
but one place, and that place is why it exists. The harvested lifecycle
|
|
49
|
+
model may answer for silence; it may not narrow a claim. A corpus only
|
|
50
|
+
covers the states somebody wrote a scenario for, so letting it answer for
|
|
51
|
+
`Unrestricted` would shrink "legal in every state" down to an accident of
|
|
52
|
+
coverage, and the command would quietly stop being offered on the rows
|
|
53
|
+
nobody tested. With the two spelled apart, that shrinkage cannot happen and
|
|
54
|
+
a scenario that genuinely refutes the claim is a contradiction instead. */
|
|
55
|
+
| Undeclared
|
|
43
56
|
/** Brings the row into existence, so there is no state it could come from.
|
|
44
57
|
Distinct from `Unrestricted`, which draws no edge at all. */
|
|
45
58
|
| Creates('state)
|
|
@@ -54,6 +67,7 @@ type t<'state> =
|
|
|
54
67
|
tells apart. */
|
|
55
68
|
let allowedStates = (transition: t<'state>): option<array<'state>> =>
|
|
56
69
|
switch transition {
|
|
70
|
+
| Undeclared
|
|
57
71
|
| Unrestricted
|
|
58
72
|
| Creates(_) => None
|
|
59
73
|
| Guards(states)
|
|
@@ -63,8 +77,21 @@ let allowedStates = (transition: t<'state>): option<array<'state>> =>
|
|
|
63
77
|
/** The state the command's handler writes, or `None` for one that moves nothing. */
|
|
64
78
|
let targetState = (transition: t<'state>): option<'state> =>
|
|
65
79
|
switch transition {
|
|
80
|
+
| Undeclared
|
|
66
81
|
| Unrestricted
|
|
67
82
|
| Guards(_) => None
|
|
68
83
|
| Creates(state)
|
|
69
84
|
| Moves(_, state) => Some(state)
|
|
70
85
|
}
|
|
86
|
+
|
|
87
|
+
/** Whether the command is claimed legal in every state, as opposed to nothing
|
|
88
|
+
being claimed at all. The two erase to the same pair of `None`s above, so this
|
|
89
|
+
is the only thing that can tell a reader of the erased form which it holds. */
|
|
90
|
+
let isUnrestricted = (transition: t<'state>): bool =>
|
|
91
|
+
switch transition {
|
|
92
|
+
| Unrestricted => true
|
|
93
|
+
| Undeclared
|
|
94
|
+
| Creates(_)
|
|
95
|
+
| Guards(_)
|
|
96
|
+
| Moves(_, _) => false
|
|
97
|
+
}
|
|
@@ -7,11 +7,11 @@ function allowedStates(transition) {
|
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
9
|
switch (transition.TAG) {
|
|
10
|
-
case "Creates" :
|
|
11
|
-
return;
|
|
12
10
|
case "Guards" :
|
|
13
11
|
case "Moves" :
|
|
14
12
|
return transition._0;
|
|
13
|
+
default:
|
|
14
|
+
return;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -22,15 +22,24 @@ function targetState(transition) {
|
|
|
22
22
|
switch (transition.TAG) {
|
|
23
23
|
case "Creates" :
|
|
24
24
|
return Primitive_option.some(transition._0);
|
|
25
|
-
case "Guards" :
|
|
26
|
-
return;
|
|
27
25
|
case "Moves" :
|
|
28
26
|
return Primitive_option.some(transition._1);
|
|
27
|
+
default:
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isUnrestricted(transition) {
|
|
33
|
+
if (typeof transition !== "object") {
|
|
34
|
+
return transition === "Unrestricted";
|
|
35
|
+
} else {
|
|
36
|
+
return false;
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export {
|
|
33
41
|
allowedStates,
|
|
34
42
|
targetState,
|
|
43
|
+
isUnrestricted,
|
|
35
44
|
}
|
|
36
45
|
/* No side effect */
|