@reventlessdev/reventless-core 3.0.0-alpha.237 → 3.0.0-alpha.239
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 +24 -0
- package/package.json +8 -8
- package/src/Message.res +1 -1
- package/src/adapter/Monitoring/Monitoring.res +1 -1
- package/src/admin/Platform_Admin_Structure.res +1 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +2 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate_Callback.res +2 -2
- package/src/components/Api/ApiAllowedStatesHelpers.res +2 -3
- package/src/components/Api/ApiTargetStateHelpers.res +4 -3
- package/src/components/Api/GraphQL_FragmentGenerator.res +122 -12
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +41 -8
- package/src/components/Api/SuryToJsonSchema.res +8 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +8 -4
- package/src/components/Dcb/Dcb_Builder.res +79 -16
- package/src/components/Dcb/Dcb_Builder.res.mjs +41 -5
- package/src/components/EventLog/EventLog.res +1 -1
- package/src/plugin/component/Plugin_Builder.res +7 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +3 -1
- package/src/plugin/component/Plugin_Structure.res +376 -27
- package/src/plugin/component/Plugin_Structure.res.mjs +175 -8
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +1 -1
- package/tests/admin/Platform_BakedManifestTest.res +1 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +1 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +3 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +6 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res +1 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +2 -0
- package/tests/aggregate/AggregateCacheTest.res +1 -1
- package/tests/aggregate/AggregateSnapshotTest.res +1 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res +171 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +108 -0
- package/tests/api/SuryToJsonSchemaTest.res +30 -3
- package/tests/api/SuryToJsonSchemaTest.res.mjs +49 -4
- package/tests/commandgenerator/OwnerStampingTest.res +57 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +19 -0
- package/tests/message/MessageTest.res +1 -1
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +1 -1
- package/tests/plugin/PluginStructureTest.res +268 -4
- package/tests/plugin/PluginStructureTest.res.mjs +284 -3
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res +31 -0
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +73 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +16 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -2
- package/tests/plugin/StateViewSlice/PsShipmentsView.res +26 -0
- package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +101 -0
- package/tests/util/CsvStreamTest.res +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
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.239 (2026-08-18)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **api:** declare the reference door in the SDL every backend is built from ([5c1857e](https://github.com/ReventlessDev/reventless-core/commit/5c1857ea90ff40305a1c44a9e57043528e6a93aa))
|
|
11
|
+
* **api:** make the by-index door answer, and let an elevated caller widen it ([0fe0c6f](https://github.com/ReventlessDev/reventless-core/commit/0fe0c6f8dec6228ecaba39577e28d780b4f79c83))
|
|
12
|
+
* **core:** fail the build on a retired state the field's enum does not declare ([781935b](https://github.com/ReventlessDev/reventless-core/commit/781935bf8f3e9c418d1e7b25e7235717e4812d7c))
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **core:** let a reference name a retired row, and let an elevated caller open one ([9e2623a](https://github.com/ReventlessDev/reventless-core/commit/9e2623a4b22487561607fcc0ca19d51726069ee4))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 3.0.0-alpha.238 (2026-08-16)
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **core:** stamp the owner from each DCB slice's own command schema ([6edbdf4](https://github.com/ReventlessDev/reventless-core/commit/6edbdf4680f2535597c3b560215aa514ceed27eb))
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* **core:** check a declared lifecycle edge against the states that exist ([d427d92](https://github.com/ReventlessDev/reventless-core/commit/d427d92f8912b4dc9a9407c3c0cec3200d41a5f1))
|
|
26
|
+
* **core:** report a lifecycle state no command can reach ([eb24dbd](https://github.com/ReventlessDev/reventless-core/commit/eb24dbd90df07185f0f4785c4bb726f5b617e7b2))
|
|
27
|
+
* **ppx:** declare a command's lifecycle edge once, as [@transition](https://github.com/transition) ([dd35130](https://github.com/ReventlessDev/reventless-core/commit/dd3513014f14b71879ee21263c6755d3c3d95096))
|
|
28
|
+
|
|
29
|
+
|
|
6
30
|
# 3.0.0-alpha.237 (2026-08-16)
|
|
7
31
|
|
|
8
32
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.239",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,21 +29,21 @@
|
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
32
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
32
33
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
33
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.7",
|
|
34
|
-
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.7",
|
|
35
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
36
|
-
"@reventlessdev/rescript-
|
|
35
|
+
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.8",
|
|
37
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
38
|
-
"@reventlessdev/rescript-
|
|
39
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
37
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.8",
|
|
38
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.145",
|
|
39
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.117",
|
|
40
40
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
41
|
-
"@reventlessdev/
|
|
41
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
45
45
|
"sury-ppx": "11.0.0-alpha.2",
|
|
46
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
46
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.69"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"rescript": "12.3.0"
|
package/src/Message.res
CHANGED
|
@@ -69,7 +69,7 @@ let toCommandSchema' = (idSchema, commandSchema) =>
|
|
|
69
69
|
// before a nested `@schema` field existed still decodes instead of bricking the
|
|
70
70
|
// aggregate's log replay. These two entry points thread the memoized envelope schema;
|
|
71
71
|
// `Reventless.Message.decode` (the other central decoder) is tolerant at the source.
|
|
72
|
-
// See docs/plans/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
72
|
+
// See docs/plans/done/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
73
73
|
let decodeEvent' = (json, idSchema, eventSchema) =>
|
|
74
74
|
json->parseJsonTolerant(toEventSchema'(idSchema, eventSchema))
|
|
75
75
|
let decodeCommand' = (json, idSchema, commandSchema) =>
|
|
@@ -10,7 +10,7 @@ concern; this only exposes the choke point through which the framework says
|
|
|
10
10
|
"I just provisioned an execution unit" and leaves what to do about it to the
|
|
11
11
|
listener.
|
|
12
12
|
|
|
13
|
-
See `docs/plans/monitoring-hook-seam.md`.
|
|
13
|
+
See `docs/plans/done/monitoring-hook-seam.md`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -29,7 +29,7 @@ let sdlTypes: array<string> = [
|
|
|
29
29
|
// structures persisted before the fields existed decode as `None`, a hand-rolled
|
|
30
30
|
// def may decline to state them, and a state whose key cannot be resolved has no
|
|
31
31
|
// `idField` to report.
|
|
32
|
-
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n lifecycleField: String\n visibility: String\n chapter: String\n singleQueryField: String\n idField: String\n idFieldSource: String\n requiredAccess: [String!]\n ownerField: String\n retiredField: String\n retiredValues: [String!]\n}`,
|
|
32
|
+
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n lifecycleField: String\n visibility: String\n chapter: String\n singleQueryField: String\n idField: String\n idFieldSource: String\n requiredAccess: [String!]\n ownerField: String\n retiredField: String\n retiredValues: [String!]\n namedWhenRetired: Boolean!\n}`,
|
|
33
33
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
|
|
34
34
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
35
35
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
@@ -130,6 +130,7 @@ let encodeQueryableDef = (r: queryableDef): JSON.t =>
|
|
|
130
130
|
("ownerField", r.ownerField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
131
131
|
("retiredField", r.retiredField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
132
132
|
("retiredValues", r.retiredValues->Option.mapOr(JSON.Encode.null, encodeStrings)),
|
|
133
|
+
("namedWhenRetired", JSON.Encode.bool(r.namedWhenRetired->Option.getOr(false))),
|
|
133
134
|
])->JSON.Encode.object
|
|
134
135
|
|
|
135
136
|
let encodeEventDef = (e: eventDef): JSON.t =>
|
|
@@ -10,7 +10,7 @@ let sdlTypes = [
|
|
|
10
10
|
`type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
11
11
|
`type Platform_ErrorDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
12
12
|
`type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n errors: [Platform_ErrorDef!]!\n chapter: String\n}`,
|
|
13
|
-
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n lifecycleField: String\n visibility: String\n chapter: String\n singleQueryField: String\n idField: String\n idFieldSource: String\n requiredAccess: [String!]\n ownerField: String\n retiredField: String\n retiredValues: [String!]\n}`,
|
|
13
|
+
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n lifecycleField: String\n visibility: String\n chapter: String\n singleQueryField: String\n idField: String\n idFieldSource: String\n requiredAccess: [String!]\n ownerField: String\n retiredField: String\n retiredValues: [String!]\n namedWhenRetired: Boolean!\n}`,
|
|
14
14
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
|
|
15
15
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
16
16
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
@@ -168,6 +168,10 @@ function encodeQueryableDef(r) {
|
|
|
168
168
|
[
|
|
169
169
|
"retiredValues",
|
|
170
170
|
Stdlib_Option.mapOr(r.retiredValues, null, encodeStrings)
|
|
171
|
+
],
|
|
172
|
+
[
|
|
173
|
+
"namedWhenRetired",
|
|
174
|
+
Stdlib_Option.getOr(r.namedWhenRetired, false)
|
|
171
175
|
]
|
|
172
176
|
]);
|
|
173
177
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// UiFragmentRegistry StateChangeSlice — the write side of the platform UI-fragment
|
|
2
2
|
// registry, extracted off the Plugin aggregate (see
|
|
3
|
-
// docs/plans/event-sourced-fragment-registries.md).
|
|
3
|
+
// docs/plans/done/event-sourced-fragment-registries.md).
|
|
4
4
|
//
|
|
5
5
|
// Commands are driven by the plugin runtime's connect handshake and routed here by the
|
|
6
6
|
// admin PluginExtensionPoint's UI-fragment mapping (RegisterUiFragment on ConnectPlugin,
|
|
@@ -114,7 +114,7 @@ module Make = (
|
|
|
114
114
|
// at append time → the conflict branch invalidates the entry and the retry
|
|
115
115
|
// replays cold. Mirrors the StateChangeSlice decision-model cache, including
|
|
116
116
|
// the fixed capacity (a per-aggregate knob is a future refinement — see
|
|
117
|
-
// docs/plans/aggregate-snapshotting.md).
|
|
117
|
+
// docs/plans/done/aggregate-snapshotting.md).
|
|
118
118
|
let replayCacheCapacity = 100
|
|
119
119
|
let replayCache: Lru.t<string, (Behavior.state, int)> = Lru.make(
|
|
120
120
|
~capacity=replayCacheCapacity,
|
|
@@ -122,7 +122,7 @@ module Make = (
|
|
|
122
122
|
|
|
123
123
|
let resetCache = () => replayCache->Lru.clear
|
|
124
124
|
|
|
125
|
-
// Persisted-snapshot configuration (docs/plans/aggregate-snapshotting.md).
|
|
125
|
+
// Persisted-snapshot configuration (docs/plans/done/aggregate-snapshotting.md).
|
|
126
126
|
// `None` (the default) keeps full replay; `Some({interval, stateSchema})`
|
|
127
127
|
// seeds cold replays from the latest persisted snapshot and writes a fresh
|
|
128
128
|
// one every `interval` events. Snapshots are a read optimization only — the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Read per-variant `allowedStates` metadata that the reventless-ppx
|
|
2
|
-
// `@
|
|
2
|
+
// an `@transition` from-set attaches to a command schema. Consumed by
|
|
3
3
|
// `Plugin_Structure.toCommandDef` when building the `commandDef` records that
|
|
4
4
|
// land in `Platform_ComponentDefinitions` so AutoUI can filter the per-row command
|
|
5
5
|
// menu by the row's status.
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
open ReventlessInfra.Api
|
|
8
8
|
|
|
9
9
|
/** Returns the allowed-state list for a given command-variant name, or None
|
|
10
|
-
when the variant
|
|
11
|
-
— the command shows on every row). */
|
|
10
|
+
when the variant declares no `@transition` (the command shows on every row). */
|
|
12
11
|
let getAllowedStates = (
|
|
13
12
|
commandSchema: S.t<unknown>,
|
|
14
13
|
~variantName: string,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Read per-variant `targetState` metadata that the reventless-ppx
|
|
2
|
-
// `@
|
|
2
|
+
// an `@transition` target attaches to a command schema. Consumed by
|
|
3
3
|
// `Plugin_Structure.toCommandDef` when building the `commandDef` records that
|
|
4
4
|
// land in `Platform_ComponentDefinitions`, so AutoUI's board drag resolver can
|
|
5
5
|
// move a row by a *declared* transition instead of a name-stem guess. Mirrors
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
open ReventlessInfra.Api
|
|
9
9
|
|
|
10
10
|
/** Returns the declared target state for a given command-variant name, or None
|
|
11
|
-
when the variant
|
|
12
|
-
|
|
11
|
+
when the variant declares no target — either because it carries no
|
|
12
|
+
`@transition` at all, or because it declares a from-set only, which is a
|
|
13
|
+
command saying it does not move the row. */
|
|
13
14
|
let getTargetState = (
|
|
14
15
|
commandSchema: S.t<unknown>,
|
|
15
16
|
~variantName: string,
|
|
@@ -411,9 +411,16 @@ let deriveObjectQueryField = (
|
|
|
411
411
|
~subIdField: option<string>=?,
|
|
412
412
|
): string =>
|
|
413
413
|
if includeIdParam {
|
|
414
|
+
// `includeRetired` reaches this door for the reason it reaches the list: a
|
|
415
|
+
// caller the server would serve a retired row to had, until it did, no way
|
|
416
|
+
// to ask for one *singly*. The archive toggle put such rows on screen and
|
|
417
|
+
// clicking one read a door that refuses them to everybody — `decideRetired`
|
|
418
|
+
// withholds from `Elevated` and `System` too until they ask, and here there
|
|
419
|
+
// was nothing to ask with.
|
|
414
420
|
switch subIdField {
|
|
415
|
-
| Some(sortField) =>
|
|
416
|
-
|
|
421
|
+
| Some(sortField) =>
|
|
422
|
+
` ${singleFieldName}(id: ID!, ${sortField}: String!, includeRetired: Boolean): ${typeName}`
|
|
423
|
+
| None => ` ${singleFieldName}(id: ID!, includeRetired: Boolean): ${typeName}`
|
|
417
424
|
}
|
|
418
425
|
} else {
|
|
419
426
|
` ${singleFieldName}: ${typeName}`
|
|
@@ -434,7 +441,38 @@ let deriveByIdsQueryField = (
|
|
|
434
441
|
~listFieldName: string,
|
|
435
442
|
~returnTypeName: string,
|
|
436
443
|
): string =>
|
|
437
|
-
` ${listFieldName}ByIds(ids: [String!]
|
|
444
|
+
` ${listFieldName}ByIds(ids: [String!]!, includeRetired: Boolean): [${returnTypeName}!]!`
|
|
445
|
+
|
|
446
|
+
// ── The reference door ─────────────────────────────────────────────────────
|
|
447
|
+
//
|
|
448
|
+
// What a caller holding a pointer to a row may learn about it: its id, the label
|
|
449
|
+
// a reference resolves to, and — where the view declares a retirement — whether
|
|
450
|
+
// this row is in one and which state that is.
|
|
451
|
+
//
|
|
452
|
+
// The narrowness is the *type's*, which is the whole reason this is a separate
|
|
453
|
+
// field rather than an argument on the by-ids door. A rule that answered a
|
|
454
|
+
// retired row on the existing door "as long as only the safe fields were asked
|
|
455
|
+
// for" would have to be re-implemented, identically, in four backends and to
|
|
456
|
+
// survive aliases, fragments and `__typename`. Here a caller cannot ask for a
|
|
457
|
+
// price, because the type has no price.
|
|
458
|
+
//
|
|
459
|
+
// Emitted for every view, not only for the ones that opted in, on exactly the
|
|
460
|
+
// reasoning `includeRetired` states above: a field that appears and disappears
|
|
461
|
+
// with an annotation makes adding or removing that annotation a breaking schema
|
|
462
|
+
// change, and forces every client to feature-detect. What the annotation decides
|
|
463
|
+
// is not whether the door exists but whether a *retired* row comes through it —
|
|
464
|
+
// without it this is a cheap label read that narrows exactly as every other door
|
|
465
|
+
// does.
|
|
466
|
+
let deriveRefTypeSdl = (~returnTypeName: string): string =>
|
|
467
|
+
`type ${returnTypeName}Ref {\n id: ID!\n label: String!\n retired: Boolean!\n retiredState: String\n}`
|
|
468
|
+
|
|
469
|
+
// `retiredState` is null in two cases that do not need telling apart by a
|
|
470
|
+
// consumer: a live row, and a boolean-form retirement, where the field is the
|
|
471
|
+
// state and `retired: true` has already said everything there is to say.
|
|
472
|
+
let deriveRefsQueryField = (
|
|
473
|
+
~listFieldName: string,
|
|
474
|
+
~returnTypeName: string,
|
|
475
|
+
): string => ` ${listFieldName}Refs(ids: [ID!]!): [${returnTypeName}Ref!]!`
|
|
438
476
|
|
|
439
477
|
// `includeRetired` sits beside the paging arguments rather than inside `filter`,
|
|
440
478
|
// and that placement is the point. `filter` is the caller's description of the
|
|
@@ -459,6 +497,63 @@ let deriveConnectionQueryField = (
|
|
|
459
497
|
` ${listFieldName}(filter: ${filterTypeName}${orderByArg}, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): ${singularTypeName}Connection!`
|
|
460
498
|
}
|
|
461
499
|
|
|
500
|
+
// ── The by-index door ──────────────────────────────────────────────────────
|
|
501
|
+
//
|
|
502
|
+
// One derivation, called by every backend that serves this field, because the
|
|
503
|
+
// alternative was tried and did not survive contact. The field used to be
|
|
504
|
+
// emitted here for AppSync and again, independently, in the local adapter, and
|
|
505
|
+
// the two agreed on neither its name, its argument nor its return type. Both
|
|
506
|
+
// were wrong in the same way: each disagreed with the resolver standing behind
|
|
507
|
+
// it. The AppSync shape declared `id: ID!` while its resolver read the index
|
|
508
|
+
// field, so the value could not be passed at all; the local shape promised
|
|
509
|
+
// `[String]` while its resolver returned whole rows, so every call failed to
|
|
510
|
+
// serialise. A door that answers on no backend is a door whose signature can be
|
|
511
|
+
// chosen on the merits, which is what the one below is.
|
|
512
|
+
|
|
513
|
+
/** The row field an index is keyed on.
|
|
514
|
+
|
|
515
|
+
For a named index (`@index("byOwner") ownerId`) the index name and the field it
|
|
516
|
+
indexes differ, and it is the *field* every resolver filters on; for an unnamed
|
|
517
|
+
index the two are the same string. */
|
|
518
|
+
let indexKeyField = (indexConfig: Reventless.ReadModel.indexConfig): string =>
|
|
519
|
+
indexConfig.idField->Option.getOr(indexConfig.index)
|
|
520
|
+
|
|
521
|
+
/** `<single>By<Index>`, dropping a leading `by` from the index name so
|
|
522
|
+
`@index("byOwner")` reads `XByOwner` rather than `XByByOwner`. */
|
|
523
|
+
let indexQueryFieldName = (~singleFieldName: string, ~index: string): string => {
|
|
524
|
+
let stripped = if index->String.startsWith("by") && index->String.length > 2 {
|
|
525
|
+
index->String.slice(~start=2, ~end=index->String.length)
|
|
526
|
+
} else {
|
|
527
|
+
index
|
|
528
|
+
}
|
|
529
|
+
singleFieldName ++ "By" ++ stripped->String.capitalize
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** The by-index door's signature.
|
|
533
|
+
|
|
534
|
+
Paging and `includeRetired` are here for the reasons `deriveConnectionQueryField`
|
|
535
|
+
gives above — this door reads a secondary index that can carry as many rows as
|
|
536
|
+
the list, and an elevated caller that can widen every other door but this one
|
|
537
|
+
would find the archive reachable by id and by list and not by the index that
|
|
538
|
+
exists to look rows up. */
|
|
539
|
+
let deriveIndexQueryField = (
|
|
540
|
+
~singleFieldName: string,
|
|
541
|
+
~indexConfig: Reventless.ReadModel.indexConfig,
|
|
542
|
+
~connectionTypeName: string,
|
|
543
|
+
): string => {
|
|
544
|
+
let fieldName = indexQueryFieldName(~singleFieldName, ~index=indexConfig.index)
|
|
545
|
+
let keyField = indexKeyField(indexConfig)
|
|
546
|
+
// An index with a sort key can be narrowed to one exact value on it. Optional,
|
|
547
|
+
// unlike the partition argument: naming the index value is what the door is
|
|
548
|
+
// for, narrowing further is a refinement. The AppSync sort template has read
|
|
549
|
+
// this argument all along, against an SDL that never offered it.
|
|
550
|
+
let sortArg = switch indexConfig.subIdField {
|
|
551
|
+
| Some(sortField) => `${sortField}: String, `
|
|
552
|
+
| None => ""
|
|
553
|
+
}
|
|
554
|
+
` ${fieldName}(${keyField}: String!, ${sortArg}first: Int, after: String, last: Int, before: String, includeRetired: Boolean): ${connectionTypeName}!`
|
|
555
|
+
}
|
|
556
|
+
|
|
462
557
|
// ── Mutation field derivation ──────────────────────────────────────────────
|
|
463
558
|
|
|
464
559
|
let deriveMutationFieldFromObject = (
|
|
@@ -652,6 +747,23 @@ let generate = (
|
|
|
652
747
|
~returnTypeName=entry.returnTypeName,
|
|
653
748
|
),
|
|
654
749
|
)
|
|
750
|
+
|
|
751
|
+
// The reference door, under the by-ids condition because it is the same
|
|
752
|
+
// read. Emitted here rather than per-backend so the field a backend
|
|
753
|
+
// provisions a resolver for is one this SDL declares: the AppSync adapter
|
|
754
|
+
// registers `<list>Refs` for every queryable on exactly that premise, and
|
|
755
|
+
// a door named in one half and not the other fails the deploy outright.
|
|
756
|
+
let refTypeName = entry.returnTypeName ++ "Ref"
|
|
757
|
+
if !(seenTypes->Set.has(refTypeName)) {
|
|
758
|
+
seenTypes->Set.add(refTypeName)
|
|
759
|
+
types->Array.push(deriveRefTypeSdl(~returnTypeName=entry.returnTypeName))
|
|
760
|
+
}
|
|
761
|
+
queries->Array.push(
|
|
762
|
+
deriveRefsQueryField(
|
|
763
|
+
~listFieldName=entry.listFieldName,
|
|
764
|
+
~returnTypeName=entry.returnTypeName,
|
|
765
|
+
),
|
|
766
|
+
)
|
|
655
767
|
}
|
|
656
768
|
|
|
657
769
|
let listFieldName = entry.listFieldName
|
|
@@ -677,17 +789,15 @@ let generate = (
|
|
|
677
789
|
switch entry.indexQueries {
|
|
678
790
|
| Some(indexes) =>
|
|
679
791
|
let connectionTypeName = entry.returnTypeName ++ "Connection"
|
|
680
|
-
indexes->Array.forEach(
|
|
681
|
-
let stripped = if index->String.startsWith("by") && index->String.length > 2 {
|
|
682
|
-
index->String.slice(~start=2, ~end=index->String.length)
|
|
683
|
-
} else {
|
|
684
|
-
index
|
|
685
|
-
}
|
|
686
|
-
let fieldName = entry.singleFieldName ++ "By" ++ stripped->String.capitalize
|
|
792
|
+
indexes->Array.forEach(indexConfig =>
|
|
687
793
|
queries->Array.push(
|
|
688
|
-
|
|
794
|
+
deriveIndexQueryField(
|
|
795
|
+
~singleFieldName=entry.singleFieldName,
|
|
796
|
+
~indexConfig,
|
|
797
|
+
~connectionTypeName,
|
|
798
|
+
),
|
|
689
799
|
)
|
|
690
|
-
|
|
800
|
+
)
|
|
691
801
|
| None => ()
|
|
692
802
|
}
|
|
693
803
|
|
|
@@ -354,9 +354,9 @@ function deriveObjectQueryField(singleFieldName, typeName, includeIdParamOpt, su
|
|
|
354
354
|
let includeIdParam = includeIdParamOpt !== undefined ? includeIdParamOpt : true;
|
|
355
355
|
if (includeIdParam) {
|
|
356
356
|
if (subIdField !== undefined) {
|
|
357
|
-
return ` ` + singleFieldName + `(id: ID!, ` + subIdField + `: String
|
|
357
|
+
return ` ` + singleFieldName + `(id: ID!, ` + subIdField + `: String!, includeRetired: Boolean): ` + typeName;
|
|
358
358
|
} else {
|
|
359
|
-
return ` ` + singleFieldName + `(id: ID
|
|
359
|
+
return ` ` + singleFieldName + `(id: ID!, includeRetired: Boolean): ` + typeName;
|
|
360
360
|
}
|
|
361
361
|
} else {
|
|
362
362
|
return ` ` + singleFieldName + `: ` + typeName;
|
|
@@ -368,7 +368,15 @@ function deriveListQueryField(listFieldName, pluralTypeName) {
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
function deriveByIdsQueryField(listFieldName, returnTypeName) {
|
|
371
|
-
return ` ` + listFieldName + `ByIds(ids: [String!]
|
|
371
|
+
return ` ` + listFieldName + `ByIds(ids: [String!]!, includeRetired: Boolean): [` + returnTypeName + `!]!`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function deriveRefTypeSdl(returnTypeName) {
|
|
375
|
+
return `type ` + returnTypeName + `Ref {\n id: ID!\n label: String!\n retired: Boolean!\n retiredState: String\n}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function deriveRefsQueryField(listFieldName, returnTypeName) {
|
|
379
|
+
return ` ` + listFieldName + `Refs(ids: [ID!]!): [` + returnTypeName + `Ref!]!`;
|
|
372
380
|
}
|
|
373
381
|
|
|
374
382
|
function deriveConnectionQueryField(listFieldName, singularTypeName, filterTypeName, hasOrderByOpt) {
|
|
@@ -377,6 +385,23 @@ function deriveConnectionQueryField(listFieldName, singularTypeName, filterTypeN
|
|
|
377
385
|
return ` ` + listFieldName + `(filter: ` + filterTypeName + orderByArg + `, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): ` + singularTypeName + `Connection!`;
|
|
378
386
|
}
|
|
379
387
|
|
|
388
|
+
function indexKeyField(indexConfig) {
|
|
389
|
+
return Stdlib_Option.getOr(indexConfig.idField, indexConfig.index);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function indexQueryFieldName(singleFieldName, index) {
|
|
393
|
+
let stripped = index.startsWith("by") && index.length > 2 ? index.slice(2, index.length) : index;
|
|
394
|
+
return singleFieldName + "By" + Stdlib_String.capitalize(stripped);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function deriveIndexQueryField(singleFieldName, indexConfig, connectionTypeName) {
|
|
398
|
+
let fieldName = indexQueryFieldName(singleFieldName, indexConfig.index);
|
|
399
|
+
let keyField = indexKeyField(indexConfig);
|
|
400
|
+
let sortField = indexConfig.subIdField;
|
|
401
|
+
let sortArg = sortField !== undefined ? sortField + `: String, ` : "";
|
|
402
|
+
return ` ` + fieldName + `(` + keyField + `: String!, ` + sortArg + `first: Int, after: String, last: Int, before: String, includeRetired: Boolean): ` + connectionTypeName + `!`;
|
|
403
|
+
}
|
|
404
|
+
|
|
380
405
|
function deriveMutationFieldFromObject(fieldName, collectedTypes, seenTypes, variantSchema) {
|
|
381
406
|
let fields = SchemaType$ReventlessCore.fromSuryObject(fieldName, variantSchema);
|
|
382
407
|
if (fields === undefined) {
|
|
@@ -481,6 +506,12 @@ function generate(mutationEntries, queryEntries) {
|
|
|
481
506
|
queries.push(singleField);
|
|
482
507
|
if (includeIdParam && entry.subIdField === undefined) {
|
|
483
508
|
queries.push(deriveByIdsQueryField(entry.listFieldName, entry.returnTypeName));
|
|
509
|
+
let refTypeName = entry.returnTypeName + "Ref";
|
|
510
|
+
if (!seenTypes.has(refTypeName)) {
|
|
511
|
+
seenTypes.add(refTypeName);
|
|
512
|
+
types.push(deriveRefTypeSdl(entry.returnTypeName));
|
|
513
|
+
}
|
|
514
|
+
queries.push(deriveRefsQueryField(entry.listFieldName, entry.returnTypeName));
|
|
484
515
|
}
|
|
485
516
|
let listFieldName = entry.listFieldName;
|
|
486
517
|
let _sf = entry.subIdField;
|
|
@@ -495,11 +526,8 @@ function generate(mutationEntries, queryEntries) {
|
|
|
495
526
|
let indexes = entry.indexQueries;
|
|
496
527
|
if (indexes !== undefined) {
|
|
497
528
|
let connectionTypeName = entry.returnTypeName + "Connection";
|
|
498
|
-
indexes.forEach(
|
|
499
|
-
|
|
500
|
-
let stripped = index.startsWith("by") && index.length > 2 ? index.slice(2, index.length) : index;
|
|
501
|
-
let fieldName = entry.singleFieldName + "By" + Stdlib_String.capitalize(stripped);
|
|
502
|
-
queries.push(` ` + fieldName + `(id: ID!, first: Int, after: String, last: Int, before: String): ` + connectionTypeName + `!`);
|
|
529
|
+
indexes.forEach(indexConfig => {
|
|
530
|
+
queries.push(deriveIndexQueryField(entry.singleFieldName, indexConfig, connectionTypeName));
|
|
503
531
|
});
|
|
504
532
|
}
|
|
505
533
|
if (connectionSpec) {
|
|
@@ -579,7 +607,12 @@ export {
|
|
|
579
607
|
deriveObjectQueryField,
|
|
580
608
|
deriveListQueryField,
|
|
581
609
|
deriveByIdsQueryField,
|
|
610
|
+
deriveRefTypeSdl,
|
|
611
|
+
deriveRefsQueryField,
|
|
582
612
|
deriveConnectionQueryField,
|
|
613
|
+
indexKeyField,
|
|
614
|
+
indexQueryFieldName,
|
|
615
|
+
deriveIndexQueryField,
|
|
583
616
|
deriveMutationFieldFromObject,
|
|
584
617
|
mutationArgTypes,
|
|
585
618
|
generate,
|
|
@@ -137,6 +137,14 @@ let mergeAnnotations = (
|
|
|
137
137
|
}
|
|
138
138
|
let entries =
|
|
139
139
|
r.label === "" ? entries : Array.concat([("label", JSON.Encode.string(r.label))], entries)
|
|
140
|
+
// `namedWhenRetired` travels only when true, on the omit-the-default rule
|
|
141
|
+
// the keys above follow: false is what every record said before the opt-in
|
|
142
|
+
// existed, and writing it would put a key on every retirement to say
|
|
143
|
+
// nothing changed.
|
|
144
|
+
let entries =
|
|
145
|
+
r.namedWhenRetired
|
|
146
|
+
? Array.concat(entries, [("namedWhenRetired", JSON.Encode.bool(true))])
|
|
147
|
+
: entries
|
|
140
148
|
obj->Dict.set("x-reventless-retired", JSON.Encode.object(Dict.fromArray(entries)))
|
|
141
149
|
| _ => ()
|
|
142
150
|
}
|
|
@@ -119,20 +119,24 @@ function mergeAnnotations(fieldSchema, fieldName, spec) {
|
|
|
119
119
|
"label",
|
|
120
120
|
r.label
|
|
121
121
|
]].concat(entries$1);
|
|
122
|
-
|
|
122
|
+
let entries$4 = r.namedWhenRetired ? entries$3.concat([[
|
|
123
|
+
"namedWhenRetired",
|
|
124
|
+
true
|
|
125
|
+
]]) : entries$3;
|
|
126
|
+
obj["x-reventless-retired"] = Object.fromEntries(entries$4);
|
|
123
127
|
}
|
|
124
128
|
let match$4 = spec.metric.find(param => param[0] === fieldName);
|
|
125
129
|
if (match$4 !== undefined) {
|
|
126
130
|
let m = match$4[1];
|
|
127
|
-
let entries$
|
|
131
|
+
let entries$5 = [[
|
|
128
132
|
"aggregate",
|
|
129
133
|
m.aggregate
|
|
130
134
|
]];
|
|
131
|
-
let entries$
|
|
135
|
+
let entries$6 = m.label === "" ? entries$5 : entries$5.concat([[
|
|
132
136
|
"label",
|
|
133
137
|
m.label
|
|
134
138
|
]]);
|
|
135
|
-
obj["x-reventless-metric"] = Object.fromEntries(entries$
|
|
139
|
+
obj["x-reventless-metric"] = Object.fromEntries(entries$6);
|
|
136
140
|
}
|
|
137
141
|
return obj;
|
|
138
142
|
}
|