@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
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,36 @@
|
|
|
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.267 (2026-09-21)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** an event mapping that commands its own source no longer hangs ([d30ce6b](https://github.com/ReventlessDev/reventless-core/commit/d30ce6b360707d933732fa2c90aba49c03dda006))
|
|
11
|
+
* feat(spec)!: typed projection keys ([fc6a018](https://github.com/ReventlessDev/reventless-core/commit/fc6a01811ba788f3289e84a435a09f77fa627e1a))
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **core:** references derived from identities, and identity checks ([7cb1376](https://github.com/ReventlessDev/reventless-core/commit/7cb1376e56cdb4e4d5ab5954cd97fd37fd3ba258))
|
|
15
|
+
|
|
16
|
+
### BREAKING CHANGES
|
|
17
|
+
|
|
18
|
+
* a projection that keys a row by a payload `string`, or uses
|
|
19
|
+
the envelope id as a `string`, converts explicitly with
|
|
20
|
+
`Target.Id.makeFromString` / `Source.Id.toString`. `StateChangeSlice.Spec` no
|
|
21
|
+
longer requires `module Id`.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# 3.0.0-alpha.266 (2026-09-21)
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* **core:** an aggregate read model is not keyed by a foreign id ([0dfed74](https://github.com/ReventlessDev/reventless-core/commit/0dfed747c4182b1fc58675b42f5d3cb3eb6309c7))
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
* **spec:** one Id type per identity ([203be38](https://github.com/ReventlessDev/reventless-core/commit/203be389b9047db0a7a0294fe3c1c04ba5119965))
|
|
33
|
+
* **spec:** the runtime reads identities ([417c5f6](https://github.com/ReventlessDev/reventless-core/commit/417c5f613b031a08c7849a81c75de6a8bbeb841b))
|
|
34
|
+
|
|
35
|
+
|
|
6
36
|
# 3.0.0-alpha.265 (2026-09-20)
|
|
7
37
|
|
|
8
38
|
**Note:** Version bump only for package @reventlessdev/reventless-core
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.267",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.21",
|
|
37
37
|
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.12",
|
|
38
38
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
39
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-
|
|
39
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.168",
|
|
40
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.37",
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.140"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
45
45
|
"sury-ppx": "11.0.0-rc.2",
|
|
46
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
46
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.81"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"rescript": "12.3.0"
|
package/src/Projection.res
CHANGED
|
@@ -6,16 +6,6 @@ module Set = Belt.Set.String
|
|
|
6
6
|
|
|
7
7
|
let log = Logger.fromEnv()
|
|
8
8
|
|
|
9
|
-
module Mapping = {
|
|
10
|
-
module MakeGenericSource = (Mapping: Reventless.Projection.Mapping): (
|
|
11
|
-
Mapper.GenericSource with type t = Mapping.sourceEvent
|
|
12
|
-
) => {
|
|
13
|
-
let name = Mapping.sourceName
|
|
14
|
-
type t = Mapping.sourceEvent
|
|
15
|
-
let decode' = json => json->Message.decodeEvent'(S.string, Mapping.sourceEventSchema)
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
9
|
// Lazy: the message thunk (and any `stateToString` it triggers) runs only when
|
|
20
10
|
// debug logging is enabled, so projection actions don't serialize state on the
|
|
21
11
|
// hot path at the default Info level.
|
package/src/Projection.res.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as Sury from "sury";
|
|
4
3
|
import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
|
|
5
4
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
6
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
@@ -22,18 +21,6 @@ import * as LogFormat$ReventlessCore from "./util/LogFormat.res.mjs";
|
|
|
22
21
|
|
|
23
22
|
let log = Logger$ReventlessCore.fromEnv();
|
|
24
23
|
|
|
25
|
-
function MakeGenericSource(Mapping) {
|
|
26
|
-
let decode$p = json => Message$ReventlessCore.decodeEvent$p(json, Sury.string, Mapping.sourceEventSchema);
|
|
27
|
-
return {
|
|
28
|
-
name: Mapping.sourceName,
|
|
29
|
-
decode$p: decode$p
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let Mapping = {
|
|
34
|
-
MakeGenericSource: MakeGenericSource
|
|
35
|
-
};
|
|
36
|
-
|
|
37
24
|
function logAction(makeStr) {
|
|
38
25
|
log.debugLazy("Projection", makeStr);
|
|
39
26
|
}
|
|
@@ -786,7 +773,6 @@ let $$Set;
|
|
|
786
773
|
export {
|
|
787
774
|
$$Set,
|
|
788
775
|
log,
|
|
789
|
-
Mapping,
|
|
790
776
|
logAction,
|
|
791
777
|
overlaysFor,
|
|
792
778
|
applyOverlays,
|
package/src/ProjectionMapper.res
CHANGED
|
@@ -34,8 +34,14 @@ module Make = (
|
|
|
34
34
|
module GenericMapping = {
|
|
35
35
|
let sourceName = M.sourceName
|
|
36
36
|
let acceptedTags = Reventless.DcbTag.extractAllVariantNames(M.sourceEventSchema)
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// The storage edge: the envelope and the table carry strings, the
|
|
38
|
+
// projection typed ids — the same strings, decoded and encoded here.
|
|
39
|
+
let decode = json => json->Message.decodeEvent'(M.SourceId.schema, M.sourceEventSchema)
|
|
40
|
+
let project = MapperNto1.makeGenericMap(decode, msg =>
|
|
41
|
+
msg
|
|
42
|
+
->M.project
|
|
43
|
+
->Reventless.Projection.mapActionId(~to_=M.targetIdToString, ~from=M.targetIdFromString)
|
|
44
|
+
)
|
|
39
45
|
}
|
|
40
46
|
module(GenericMapping: Mapping)
|
|
41
47
|
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
4
|
+
import * as Projection$Reventless from "@reventlessdev/reventless-spec/src/types/Projection.res.mjs";
|
|
4
5
|
import * as Message$ReventlessCore from "./Message.res.mjs";
|
|
5
6
|
import * as MapperNto1$ReventlessCore from "./MapperNto1.res.mjs";
|
|
6
|
-
import * as Projection$ReventlessCore from "./Projection.res.mjs";
|
|
7
7
|
|
|
8
8
|
function MakeGenericTargetFromStateTarget(StateTarget) {
|
|
9
9
|
let decode = json => Message$ReventlessCore.decode(json, StateTarget.stateSchema);
|
|
@@ -27,8 +27,8 @@ function Make(Target) {
|
|
|
27
27
|
};
|
|
28
28
|
let mappings = Mappings.mappings.map(M => {
|
|
29
29
|
let acceptedTags = DcbTag$Reventless.extractAllVariantNames(M.sourceEventSchema);
|
|
30
|
-
let
|
|
31
|
-
let project = MapperNto1$ReventlessCore.makeGenericMap(
|
|
30
|
+
let decode = json => Message$ReventlessCore.decodeEvent$p(json, M.SourceId.schema, M.sourceEventSchema);
|
|
31
|
+
let project = MapperNto1$ReventlessCore.makeGenericMap(decode, msg => Projection$Reventless.mapActionId(M.project(msg), M.targetIdToString, M.targetIdFromString));
|
|
32
32
|
return {
|
|
33
33
|
sourceName: M.sourceName,
|
|
34
34
|
acceptedTags: acceptedTags,
|
|
@@ -7,7 +7,7 @@ import * as PluginsReadModelSpec$ReventlessCore from "../plugin/lifecycle/Plugin
|
|
|
7
7
|
|
|
8
8
|
let pluginId = "Platform";
|
|
9
9
|
|
|
10
|
-
let pluginCommands = Plugin_Structure$ReventlessCore.extractCommandDefs(true, undefined, variantName => Api_Naming$ReventlessCore.adminField(PluginSpec$ReventlessCore.name + "_" + variantName), PluginSpec$ReventlessCore.commandAuthorization, PluginSpec$ReventlessCore.commandTransition, undefined, PluginSpec$ReventlessCore.commandSchema);
|
|
10
|
+
let pluginCommands = Plugin_Structure$ReventlessCore.extractCommandDefs(undefined, true, undefined, variantName => Api_Naming$ReventlessCore.adminField(PluginSpec$ReventlessCore.name + "_" + variantName), PluginSpec$ReventlessCore.commandAuthorization, PluginSpec$ReventlessCore.commandTransition, undefined, PluginSpec$ReventlessCore.commandSchema);
|
|
11
11
|
|
|
12
12
|
let pluginAggregate_producedEventTypes = [];
|
|
13
13
|
|
|
@@ -40,6 +40,8 @@ let name = "UiFragments";
|
|
|
40
40
|
|
|
41
41
|
let Id;
|
|
42
42
|
|
|
43
|
+
let Key;
|
|
44
|
+
|
|
43
45
|
let subIdConfig;
|
|
44
46
|
|
|
45
47
|
let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
|
|
@@ -54,6 +56,7 @@ export {
|
|
|
54
56
|
stateSchema,
|
|
55
57
|
consumedEventSchema,
|
|
56
58
|
config,
|
|
59
|
+
Key,
|
|
57
60
|
subIdConfig,
|
|
58
61
|
moduleUrl,
|
|
59
62
|
authorization,
|
|
@@ -316,19 +316,32 @@ type keyFieldResolution =
|
|
|
316
316
|
| Ambiguous({candidates: array<string>, conventional: string})
|
|
317
317
|
| NoCandidate
|
|
318
318
|
|
|
319
|
+
/** Where a view's row key lives. A read model's row is keyed by the envelope id of
|
|
320
|
+
the events it folds, so a `*Id` field in its state is somebody else's id unless
|
|
321
|
+
it is declared or named as the view's own. A StateViewSlice keys its row by a
|
|
322
|
+
state field. */
|
|
323
|
+
type rowKeying = RowKeyedByEnvelope | RowKeyedByStateField
|
|
324
|
+
|
|
319
325
|
/**
|
|
320
326
|
The field that identifies a row, and which rung answered:
|
|
321
327
|
|
|
322
328
|
- `"annotation"` — the state declares `@id`. Nothing outranks it.
|
|
323
|
-
- `"convention"` — a field
|
|
324
|
-
(`Products` → `productId`).
|
|
325
|
-
- `"sole"` — the state has exactly one `*Id` field (`AvailableProducts`).
|
|
329
|
+
- `"convention"` — a field keyed `<singular entity name>Id` exists
|
|
330
|
+
(`Products` → `productId`): named so, or typed as that identity.
|
|
331
|
+
- `"sole"` — the state has exactly one `*Id` field (`AvailableProducts`). Only for
|
|
332
|
+
`RowKeyedByStateField`: a read model's lone `*Id` is a foreign id (`Orders` →
|
|
333
|
+
`customerId`), and having none of its own is its ordinary shape, so neither
|
|
334
|
+
that nor several `*Id` fields is a gap.
|
|
326
335
|
|
|
327
336
|
Convention outranks sole so a view carrying one foreign key and no key of its own
|
|
328
337
|
is not keyed by the foreign key. `resolveKeyField` is this, with both gaps
|
|
329
338
|
flattened to `None`.
|
|
330
339
|
*/
|
|
331
|
-
let classifyKeyField = (
|
|
340
|
+
let classifyKeyField = (
|
|
341
|
+
~rowKeying: rowKeying,
|
|
342
|
+
~entityName: string,
|
|
343
|
+
schema: S.t<unknown>,
|
|
344
|
+
): keyFieldResolution => {
|
|
332
345
|
let declared = switch Reventless.StateAnnotations.getSpec(schema) {
|
|
333
346
|
| Some({ids}) => ids->Array.get(0)
|
|
334
347
|
| None => None
|
|
@@ -336,29 +349,45 @@ let classifyKeyField = (~entityName: string, schema: S.t<unknown>): keyFieldReso
|
|
|
336
349
|
switch declared {
|
|
337
350
|
| Some(field) => Resolved({field, rung: "annotation"})
|
|
338
351
|
| None =>
|
|
352
|
+
// A field is a candidate by its name or by its type, and its key is its
|
|
353
|
+
// identity when it has one: `orderId: CustomerId.t` never keys `Orders`, and
|
|
354
|
+
// `order: OrderId.t` does.
|
|
355
|
+
let properties = switch schema {
|
|
356
|
+
| Object({properties}) => properties
|
|
357
|
+
| _ => Dict.make()
|
|
358
|
+
}
|
|
359
|
+
let keyOf = field =>
|
|
360
|
+
properties->Dict.get(field)->Option.flatMap(Reventless.Semantic.identityKey)
|
|
339
361
|
let candidates =
|
|
340
362
|
SchemaType.fromSuryObject(~typeName="", schema)
|
|
341
363
|
->Option.getOr(Dict.make())
|
|
342
364
|
->Dict.keysToArray
|
|
343
|
-
->Array.filter(isKeyFieldName)
|
|
365
|
+
->Array.filter(field => isKeyFieldName(field) || keyOf(field)->Option.isSome)
|
|
344
366
|
let singular = entityName->Api_Naming.stripViewSuffix->Api_Naming.singularize
|
|
345
367
|
let conventional =
|
|
346
368
|
singular->String.slice(~start=0, ~end=1)->String.toLowerCase ++
|
|
347
369
|
singular->String.slice(~start=1, ~end=singular->String.length) ++ "Id"
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
370
|
+
switch candidates->Array.find(field => keyOf(field)->Option.getOr(field) == conventional) {
|
|
371
|
+
| Some(field) => Resolved({field, rung: "convention"})
|
|
372
|
+
| None =>
|
|
373
|
+
if rowKeying == RowKeyedByEnvelope {
|
|
374
|
+
NoCandidate
|
|
375
|
+
} else if candidates->Array.length == 1 {
|
|
376
|
+
Resolved({field: candidates->Array.getUnsafe(0), rung: "sole"})
|
|
377
|
+
} else if Array.length(candidates) == 0 {
|
|
378
|
+
NoCandidate
|
|
379
|
+
} else {
|
|
380
|
+
Ambiguous({candidates, conventional})
|
|
381
|
+
}
|
|
356
382
|
}
|
|
357
383
|
}
|
|
358
384
|
}
|
|
359
385
|
|
|
360
|
-
let resolveKeyField = (~entityName: string, schema: S.t<unknown>): option<(
|
|
361
|
-
|
|
386
|
+
let resolveKeyField = (~rowKeying: rowKeying, ~entityName: string, schema: S.t<unknown>): option<(
|
|
387
|
+
string,
|
|
388
|
+
string,
|
|
389
|
+
)> =>
|
|
390
|
+
switch classifyKeyField(~rowKeying, ~entityName, schema) {
|
|
362
391
|
| Resolved({field, rung}) => Some((field, rung))
|
|
363
392
|
| Ambiguous(_) | NoCandidate => None
|
|
364
393
|
}
|
|
@@ -457,7 +486,9 @@ let deriveServerCapability = (~entityName: string, schema: S.t<unknown>): server
|
|
|
457
486
|
// a client asked for happened client-side over one page. Its key is knowable
|
|
458
487
|
// without the annotation in the common cases; take it. Pushed last, and both
|
|
459
488
|
// pushes dedupe, so a declared `@id` keeps its position and this is a no-op.
|
|
460
|
-
|
|
489
|
+
// Read as a state-field key for every view: a read model's lone foreign id is
|
|
490
|
+
// not its key, but it stays filterable, since dropping it would break the SDL.
|
|
491
|
+
switch resolveKeyField(~rowKeying=RowKeyedByStateField, ~entityName, schema) {
|
|
461
492
|
| Some((field, _rung)) =>
|
|
462
493
|
pushFilter(field, ~range=false)
|
|
463
494
|
pushSort(field)
|
|
@@ -5,6 +5,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
5
5
|
import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
|
|
6
6
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
7
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
|
+
import * as Semantic$Reventless from "@reventlessdev/reventless-spec/src/semantic/Semantic.res.mjs";
|
|
8
9
|
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
9
10
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
10
11
|
import * as Api_Naming$ReventlessCore from "./Api_Naming.res.mjs";
|
|
@@ -256,7 +257,7 @@ function isKeyFieldName(name) {
|
|
|
256
257
|
}
|
|
257
258
|
}
|
|
258
259
|
|
|
259
|
-
function classifyKeyField(entityName, schema) {
|
|
260
|
+
function classifyKeyField(rowKeying, entityName, schema) {
|
|
260
261
|
let match = StateAnnotations$Reventless.getSpec(schema);
|
|
261
262
|
let declared = match !== undefined ? match.ids[0] : undefined;
|
|
262
263
|
if (declared !== undefined) {
|
|
@@ -266,15 +267,27 @@ function classifyKeyField(entityName, schema) {
|
|
|
266
267
|
rung: "annotation"
|
|
267
268
|
};
|
|
268
269
|
}
|
|
269
|
-
let
|
|
270
|
+
let properties;
|
|
271
|
+
properties = schema.type === "object" ? schema.properties : ({});
|
|
272
|
+
let keyOf = field => Stdlib_Option.flatMap(properties[field], Semantic$Reventless.identityKey);
|
|
273
|
+
let candidates = Object.keys(Stdlib_Option.getOr(SchemaType$ReventlessCore.fromSuryObject("", schema), {})).filter(field => {
|
|
274
|
+
if (isKeyFieldName(field)) {
|
|
275
|
+
return true;
|
|
276
|
+
} else {
|
|
277
|
+
return Stdlib_Option.isSome(keyOf(field));
|
|
278
|
+
}
|
|
279
|
+
});
|
|
270
280
|
let singular = Api_Naming$ReventlessCore.singularize(Api_Naming$ReventlessCore.stripViewSuffix(entityName));
|
|
271
281
|
let conventional = singular.slice(0, 1).toLowerCase() + singular.slice(1, singular.length) + "Id";
|
|
272
|
-
|
|
282
|
+
let field = candidates.find(field => Stdlib_Option.getOr(keyOf(field), field) === conventional);
|
|
283
|
+
if (field !== undefined) {
|
|
273
284
|
return {
|
|
274
285
|
TAG: "Resolved",
|
|
275
|
-
field:
|
|
286
|
+
field: field,
|
|
276
287
|
rung: "convention"
|
|
277
288
|
};
|
|
289
|
+
} else if (rowKeying === "RowKeyedByEnvelope") {
|
|
290
|
+
return "NoCandidate";
|
|
278
291
|
} else if (candidates.length === 1) {
|
|
279
292
|
return {
|
|
280
293
|
TAG: "Resolved",
|
|
@@ -292,8 +305,8 @@ function classifyKeyField(entityName, schema) {
|
|
|
292
305
|
}
|
|
293
306
|
}
|
|
294
307
|
|
|
295
|
-
function resolveKeyField(entityName, schema) {
|
|
296
|
-
let match = classifyKeyField(entityName, schema);
|
|
308
|
+
function resolveKeyField(rowKeying, entityName, schema) {
|
|
309
|
+
let match = classifyKeyField(rowKeying, entityName, schema);
|
|
297
310
|
if (typeof match !== "object" || match.TAG !== "Resolved") {
|
|
298
311
|
return;
|
|
299
312
|
} else {
|
|
@@ -373,7 +386,7 @@ function deriveServerCapability(entityName, schema) {
|
|
|
373
386
|
spec.scan.forEach(name => pushFilter(name, false));
|
|
374
387
|
spec.scanSort.forEach(pushSort);
|
|
375
388
|
}
|
|
376
|
-
let match = resolveKeyField(entityName, schema);
|
|
389
|
+
let match = resolveKeyField("RowKeyedByStateField", entityName, schema);
|
|
377
390
|
if (match !== undefined) {
|
|
378
391
|
let field = match[0];
|
|
379
392
|
pushFilter(field, false);
|
|
@@ -45,6 +45,14 @@ let isTagged = Reventless.DcbTag.isTagged
|
|
|
45
45
|
let isDateTime = Reventless.DateTime.isDateTime
|
|
46
46
|
let isCalendarDate = Reventless.CalendarDate.isCalendarDate
|
|
47
47
|
|
|
48
|
+
// On the schema itself, not through `option<…>` as `Semantic.get` reads: the
|
|
49
|
+
// optional wrapper then reaches the `AnyOf` branch and stays `Nullable`.
|
|
50
|
+
let isIdentity = (schema: S.t<unknown>): bool =>
|
|
51
|
+
switch schema->S.Metadata.get(~id=Reventless.Semantic.semanticId) {
|
|
52
|
+
| Some({id}) => id === Reventless.Semantic.Id.identity
|
|
53
|
+
| None => false
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
let isIdFieldName = (name: string): bool => {
|
|
49
57
|
let lower = String.toLowerCase(name)
|
|
50
58
|
let len = String.length(lower)
|
|
@@ -123,12 +131,14 @@ and withoutDefault = (
|
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
and shapeOf = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): schemaType => {
|
|
126
|
-
//
|
|
134
|
+
// Three independent facts, checked independently. A DCB-tagged field is an
|
|
127
135
|
// entity id because it routes; a reference field is one because it points at
|
|
128
|
-
// an entity. Neither implies the other — a `@partitionTag` field carries no
|
|
136
|
+
// an entity; an identity-typed field is one whatever it is called. Neither implies the other — a `@partitionTag` field carries no
|
|
129
137
|
// reference, and `Reference.toWithoutDcbTag` carries no tag — so collapsing
|
|
130
138
|
// these into one test would silently reclassify whichever case it dropped.
|
|
131
|
-
if
|
|
139
|
+
if (
|
|
140
|
+
isTagged(schema) || Reventless.Reference.getTarget(schema)->Option.isSome || isIdentity(schema)
|
|
141
|
+
) {
|
|
132
142
|
EntityId
|
|
133
143
|
} else {
|
|
134
144
|
switch schema {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Sury from "sury";
|
|
3
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
@@ -11,6 +12,15 @@ import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/comp
|
|
|
11
12
|
import * as CalendarDate$Reventless from "@reventlessdev/reventless-spec/src/semantic/CalendarDate.res.mjs";
|
|
12
13
|
import * as FieldDefault$Reventless from "@reventlessdev/reventless-spec/src/components/FieldDefault.res.mjs";
|
|
13
14
|
|
|
15
|
+
function isIdentity(schema) {
|
|
16
|
+
let match = Sury.$Metadata_get(schema, Semantic$Reventless.semanticId);
|
|
17
|
+
if (match !== undefined) {
|
|
18
|
+
return match.id === Semantic$Reventless.Id.identity;
|
|
19
|
+
} else {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
function isIdFieldName(name) {
|
|
15
25
|
let lower = name.toLowerCase();
|
|
16
26
|
let len = lower.length;
|
|
@@ -87,7 +97,7 @@ function withoutDefault(parentName, fieldName, schema) {
|
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
function shapeOf(parentName, fieldName, schema) {
|
|
90
|
-
if (DcbTag$Reventless.isTagged(schema) || Stdlib_Option.isSome(Reference$Reventless.getTarget(schema))) {
|
|
100
|
+
if (DcbTag$Reventless.isTagged(schema) || Stdlib_Option.isSome(Reference$Reventless.getTarget(schema)) || isIdentity(schema)) {
|
|
91
101
|
return "EntityId";
|
|
92
102
|
}
|
|
93
103
|
switch (schema.type) {
|
|
@@ -425,6 +435,7 @@ export {
|
|
|
425
435
|
isTagged,
|
|
426
436
|
isDateTime,
|
|
427
437
|
isCalendarDate,
|
|
438
|
+
isIdentity,
|
|
428
439
|
isIdFieldName,
|
|
429
440
|
isIdsFieldName,
|
|
430
441
|
semanticCompositeNames,
|
|
@@ -439,4 +450,4 @@ export {
|
|
|
439
450
|
trailShape,
|
|
440
451
|
fromSuryObject,
|
|
441
452
|
}
|
|
442
|
-
/*
|
|
453
|
+
/* sury Not a pure module */
|
|
@@ -310,11 +310,15 @@ and withSemantic = (fieldSchema: JSON.t, sem: Reventless.Semantic.t): JSON.t =>
|
|
|
310
310
|
obj->Dict.set("x-reventless-semantic-source", str("type"))
|
|
311
311
|
switch sem.payload {
|
|
312
312
|
| Plain => ()
|
|
313
|
-
| ReferenceTo({entity, plugin}) =>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
| ReferenceTo({entity, plugin, ?identity}) =>
|
|
314
|
+
let pairs = withOptionalPlugin([("entity", str(entity))], plugin)
|
|
315
|
+
let pairs = switch identity {
|
|
316
|
+
| Some(key) => Array.concat(pairs, [("identity", str(key))])
|
|
317
|
+
| None => pairs
|
|
318
|
+
}
|
|
319
|
+
obj->Dict.set("x-reventless-semantic-target", jsonObject(pairs))
|
|
320
|
+
| IdentityOf({key}) =>
|
|
321
|
+
obj->Dict.set("x-reventless-semantic-target", jsonObject([("key", str(key))]))
|
|
318
322
|
| StoredIn({plugin, store}) =>
|
|
319
323
|
// An absent plugin means "this plugin's own store" — omit the key rather
|
|
320
324
|
// than writing an empty string, so a reader never has to tell those apart.
|
|
@@ -355,10 +355,16 @@ function withSemantic(fieldSchema, sem) {
|
|
|
355
355
|
switch (match.TAG) {
|
|
356
356
|
case "ReferenceTo" :
|
|
357
357
|
let match$1 = match._0;
|
|
358
|
-
|
|
358
|
+
let identity = match$1.identity;
|
|
359
|
+
let pairs = withOptionalPlugin([[
|
|
359
360
|
"entity",
|
|
360
361
|
match$1.entity
|
|
361
|
-
]], match$1.plugin)
|
|
362
|
+
]], match$1.plugin);
|
|
363
|
+
let pairs$1 = identity !== undefined ? pairs.concat([[
|
|
364
|
+
"identity",
|
|
365
|
+
identity
|
|
366
|
+
]]) : pairs;
|
|
367
|
+
obj["x-reventless-semantic-target"] = Object.fromEntries(pairs$1);
|
|
362
368
|
break;
|
|
363
369
|
case "StoredIn" :
|
|
364
370
|
let match$2 = match._0;
|
|
@@ -384,6 +390,12 @@ function withSemantic(fieldSchema, sem) {
|
|
|
384
390
|
match$3.field
|
|
385
391
|
]], match$3.plugin), "view", match$3.view), "content", match$3.content));
|
|
386
392
|
break;
|
|
393
|
+
case "IdentityOf" :
|
|
394
|
+
obj["x-reventless-semantic-target"] = Object.fromEntries([[
|
|
395
|
+
"key",
|
|
396
|
+
match.key
|
|
397
|
+
]]);
|
|
398
|
+
break;
|
|
387
399
|
}
|
|
388
400
|
}
|
|
389
401
|
return obj;
|
|
@@ -79,8 +79,26 @@ module Make = (
|
|
|
79
79
|
~opts,
|
|
80
80
|
)
|
|
81
81
|
|
|
82
|
+
// Not awaited, for the reason AutomationSlice_Builder gives: a mapping that
|
|
83
|
+
// commands its own source (Placed → Ship) appends an event this handler's
|
|
84
|
+
// fiber must dequeue, while the fiber is still waiting on the command. The
|
|
85
|
+
// bus then waits on itself, and so does the request that started it. Only
|
|
86
|
+
// the local runtime runs this handler; a deployed one uses its entry point.
|
|
87
|
+
let publishJsonsDetached = jsons => {
|
|
88
|
+
let _ = publishJsons(jsons)->Promise.catch(exn => {
|
|
89
|
+
log.error(
|
|
90
|
+
~comp="EventMapper",
|
|
91
|
+
`detached publish error: ${exn
|
|
92
|
+
->JsExn.fromException
|
|
93
|
+
->Option.flatMap(JsExn.message)
|
|
94
|
+
->Option.getOr("unknown")}`,
|
|
95
|
+
)
|
|
96
|
+
Promise.resolve()
|
|
97
|
+
})
|
|
98
|
+
Promise.resolve()
|
|
99
|
+
}
|
|
82
100
|
module EventCollectorHandler = EventMapper_Callback.MakeEventCollectorHandler({
|
|
83
|
-
let publishJsons =
|
|
101
|
+
let publishJsons = publishJsonsDetached
|
|
84
102
|
let count = count
|
|
85
103
|
let addToCounterTarget = addToCounterTarget
|
|
86
104
|
let commonEventsHandler = CounterHandler.commonEventsHandler
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
6
7
|
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
8
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
7
9
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
8
10
|
import * as Counter$ReventlessCore from "../Counter/Counter.res.mjs";
|
|
9
11
|
import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
@@ -53,8 +55,15 @@ function Make(Target) {
|
|
|
53
55
|
kind: "EventMapper",
|
|
54
56
|
name: name
|
|
55
57
|
}, opts);
|
|
58
|
+
let publishJsonsDetached = jsons => {
|
|
59
|
+
Stdlib_Promise.$$catch(publishJsons(jsons), exn => {
|
|
60
|
+
log.error("EventMapper", undefined, `detached publish error: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown"));
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
});
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
};
|
|
56
65
|
let EventCollectorHandler = EventMapper_Callback$ReventlessCore.MakeEventCollectorHandler({
|
|
57
|
-
publishJsons:
|
|
66
|
+
publishJsons: publishJsonsDetached,
|
|
58
67
|
count: param.count,
|
|
59
68
|
addToCounterTarget: param.addToCounterTarget,
|
|
60
69
|
commonEventsHandler: CounterHandler.commonEventsHandler
|
|
@@ -124,7 +124,17 @@ module Make = (
|
|
|
124
124
|
let (eventType, dataDict) = rawEvent->Message.splitMessage
|
|
125
125
|
switch decoder.decode(~eventType, ~data=dataDict) {
|
|
126
126
|
| Some(event) =>
|
|
127
|
-
let actions = try Projection.project({
|
|
127
|
+
let actions = try Projection.project({
|
|
128
|
+
event,
|
|
129
|
+
meta,
|
|
130
|
+
recordedAt,
|
|
131
|
+
})->Array.map(
|
|
132
|
+
Reventless.Projection.mapActionId(
|
|
133
|
+
_,
|
|
134
|
+
~to_=Spec.Key.toString,
|
|
135
|
+
~from=Spec.Key.makeFromString,
|
|
136
|
+
),
|
|
137
|
+
) catch {
|
|
128
138
|
| exn =>
|
|
129
139
|
let errMsg =
|
|
130
140
|
exn
|
|
@@ -12,6 +12,7 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
|
|
|
12
12
|
import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
|
|
13
13
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
14
14
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
15
|
+
import * as Projection$Reventless from "@reventlessdev/reventless-spec/src/types/Projection.res.mjs";
|
|
15
16
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
16
17
|
import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
17
18
|
import * as LogFormat$ReventlessCore from "../../util/LogFormat.res.mjs";
|
|
@@ -108,7 +109,7 @@ function Make(RuntimeEnvironment) {
|
|
|
108
109
|
event: Primitive_option.valFromOption(event),
|
|
109
110
|
meta: meta,
|
|
110
111
|
recordedAt: recordedAt
|
|
111
|
-
});
|
|
112
|
+
}).map(__x => Projection$Reventless.mapActionId(__x, Spec.Key.toString, Spec.Key.makeFromString));
|
|
112
113
|
} catch (raw_exn) {
|
|
113
114
|
let exn$1 = Primitive_exceptions.internalToException(raw_exn);
|
|
114
115
|
let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn$1), Stdlib_JsExn.message), "unknown");
|
|
@@ -40,7 +40,14 @@ module Make = (
|
|
|
40
40
|
| Some(event) =>
|
|
41
41
|
idx := idx.contents + 1
|
|
42
42
|
let id = raw.tags->Array.get(0)->Option.map(tag => tag.value)->Option.getOr("?")
|
|
43
|
-
let actions =
|
|
43
|
+
let actions =
|
|
44
|
+
Projection.project({event, meta: raw.meta, recordedAt: raw.recordedAt})->Array.map(
|
|
45
|
+
Reventless.Projection.mapActionId(
|
|
46
|
+
_,
|
|
47
|
+
~to_=Spec.Key.toString,
|
|
48
|
+
~from=Spec.Key.makeFromString,
|
|
49
|
+
),
|
|
50
|
+
)
|
|
44
51
|
let actionsStr = LogFormat.actionNames(actions)
|
|
45
52
|
EffectLogger.logInfo(
|
|
46
53
|
~comp,
|
|
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
8
8
|
import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
|
|
9
|
+
import * as Projection$Reventless from "@reventlessdev/reventless-spec/src/types/Projection.res.mjs";
|
|
9
10
|
import * as LogFormat$ReventlessCore from "../../util/LogFormat.res.mjs";
|
|
10
11
|
import * as Projection$ReventlessCore from "../../Projection.res.mjs";
|
|
11
12
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
@@ -32,7 +33,7 @@ function Make(Spec) {
|
|
|
32
33
|
event: event,
|
|
33
34
|
meta: raw.meta,
|
|
34
35
|
recordedAt: raw.recordedAt
|
|
35
|
-
});
|
|
36
|
+
}).map(__x => Projection$Reventless.mapActionId(__x, Spec.Key.toString, Spec.Key.makeFromString));
|
|
36
37
|
let actionsStr = LogFormat$ReventlessCore.actionNames(actions);
|
|
37
38
|
Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, raw.data, `handling event ` + idx.contents.toString() + `/` + count + `: ` + LogFormat$ReventlessCore.bold(raw.eventType) + `(` + id + `) ` + actionsStr));
|
|
38
39
|
allActions.push(...actions.map(__x => Projection$ReventlessCore.rewriteAction(__x, raw.meta.time, Spec.stateSchema)));
|