@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +5 -5
  3. package/src/Projection.res +0 -10
  4. package/src/Projection.res.mjs +0 -14
  5. package/src/ProjectionMapper.res +8 -2
  6. package/src/ProjectionMapper.res.mjs +3 -3
  7. package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
  8. package/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs +3 -0
  9. package/src/components/Api/GraphQL_FragmentGenerator.res +47 -16
  10. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +20 -7
  11. package/src/components/Api/SchemaType.res +13 -3
  12. package/src/components/Api/SchemaType.res.mjs +13 -2
  13. package/src/components/Api/SuryToJsonSchema.res +9 -5
  14. package/src/components/Api/SuryToJsonSchema.res.mjs +14 -2
  15. package/src/components/EventMapper/EventMapper_Builder.res +19 -1
  16. package/src/components/EventMapper/EventMapper_Builder.res.mjs +10 -1
  17. package/src/components/StateViewSlice/StateViewSlice_Builder.res +11 -1
  18. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +2 -1
  19. package/src/components/StateViewSlice/StateViewSlice_Callback.res +8 -1
  20. package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +2 -1
  21. package/src/plugin/component/Plugin_Structure.res +148 -25
  22. package/src/plugin/component/Plugin_Structure.res.mjs +114 -30
  23. package/tests/api/GraphQL_FragmentGeneratorTest.res +76 -1
  24. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +78 -17
  25. package/tests/api/IdentitySemanticTest.res +77 -0
  26. package/tests/api/IdentitySemanticTest.res.mjs +71 -0
  27. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +0 -1
  28. package/tests/dcb/DcbStateChangeSliceTest.res.mjs +0 -3
  29. package/tests/eventmapper/EventMapperFixtures.res +2 -2
  30. package/tests/eventmapper/EventMapperFixtures.res.mjs +14 -2
  31. package/tests/plugin/IdentityReferenceTest.res +99 -0
  32. package/tests/plugin/IdentityReferenceTest.res.mjs +169 -0
  33. package/tests/plugin/PluginStructureAccessTest.res.mjs +14 -1
  34. package/tests/plugin/PluginStructureTest.res.mjs +42 -10
  35. package/tests/plugin/StateView/PsAnnotatedView.res.mjs +3 -0
  36. package/tests/plugin/StateView/PsAvailableProductsView.res.mjs +3 -0
  37. package/tests/plugin/StateView/PsCategoriesView.res.mjs +3 -0
  38. package/tests/plugin/StateView/PsCustomersView.res.mjs +3 -0
  39. package/tests/plugin/StateView/PsGatedView.res.mjs +3 -0
  40. package/tests/plugin/StateView/PsOrdersView.res.mjs +3 -0
  41. 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.265",
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.166",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.138",
41
- "@reventlessdev/reventless-interop": "3.0.0-alpha.37"
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.80"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.81"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "12.3.0"
@@ -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.
@@ -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,
@@ -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
- module Source = Projection.Mapping.MakeGenericSource(M)
38
- let project = MapperNto1.makeGenericMap(Source.decode', M.project)
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 Source = Projection$ReventlessCore.Mapping.MakeGenericSource(M);
31
- let project = MapperNto1$ReventlessCore.makeGenericMap(Source.decode$p, M.project);
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 named `<singular entity name>Id` exists
324
- (`Products` → `productId`).
325
- - `"sole"` — the state has exactly one `*Id` field (`AvailableProducts`).
329
+ - `"convention"` — a field keyed `<singular entity name>Id` exists
330
+ (`Products` → `productId`): named so, or typed as that identity.
331
+ - `"sole"` — the state has exactly one `*Id` field (`AvailableProducts`). Only for
332
+ `RowKeyedByStateField`: a read model's lone `*Id` is a foreign id (`Orders` →
333
+ `customerId`), and having none of its own is its ordinary shape, so neither
334
+ that nor several `*Id` fields is a gap.
326
335
 
327
336
  Convention outranks sole so a view carrying one foreign key and no key of its own
328
337
  is not keyed by the foreign key. `resolveKeyField` is this, with both gaps
329
338
  flattened to `None`.
330
339
  */
331
- let classifyKeyField = (~entityName: string, schema: S.t<unknown>): keyFieldResolution => {
340
+ let classifyKeyField = (
341
+ ~rowKeying: rowKeying,
342
+ ~entityName: string,
343
+ schema: S.t<unknown>,
344
+ ): keyFieldResolution => {
332
345
  let declared = switch Reventless.StateAnnotations.getSpec(schema) {
333
346
  | Some({ids}) => ids->Array.get(0)
334
347
  | None => None
@@ -336,29 +349,45 @@ let classifyKeyField = (~entityName: string, schema: S.t<unknown>): keyFieldReso
336
349
  switch declared {
337
350
  | Some(field) => Resolved({field, rung: "annotation"})
338
351
  | None =>
352
+ // A field is a candidate by its name or by its type, and its key is its
353
+ // identity when it has one: `orderId: CustomerId.t` never keys `Orders`, and
354
+ // `order: OrderId.t` does.
355
+ let properties = switch schema {
356
+ | Object({properties}) => properties
357
+ | _ => Dict.make()
358
+ }
359
+ let keyOf = field =>
360
+ properties->Dict.get(field)->Option.flatMap(Reventless.Semantic.identityKey)
339
361
  let candidates =
340
362
  SchemaType.fromSuryObject(~typeName="", schema)
341
363
  ->Option.getOr(Dict.make())
342
364
  ->Dict.keysToArray
343
- ->Array.filter(isKeyFieldName)
365
+ ->Array.filter(field => isKeyFieldName(field) || keyOf(field)->Option.isSome)
344
366
  let singular = entityName->Api_Naming.stripViewSuffix->Api_Naming.singularize
345
367
  let conventional =
346
368
  singular->String.slice(~start=0, ~end=1)->String.toLowerCase ++
347
369
  singular->String.slice(~start=1, ~end=singular->String.length) ++ "Id"
348
- if candidates->Array.includes(conventional) {
349
- Resolved({field: conventional, rung: "convention"})
350
- } else if candidates->Array.length == 1 {
351
- Resolved({field: candidates->Array.getUnsafe(0), rung: "sole"})
352
- } else if Array.length(candidates) == 0 {
353
- NoCandidate
354
- } else {
355
- Ambiguous({candidates, conventional})
370
+ switch candidates->Array.find(field => keyOf(field)->Option.getOr(field) == conventional) {
371
+ | Some(field) => Resolved({field, rung: "convention"})
372
+ | None =>
373
+ if rowKeying == RowKeyedByEnvelope {
374
+ NoCandidate
375
+ } else if candidates->Array.length == 1 {
376
+ Resolved({field: candidates->Array.getUnsafe(0), rung: "sole"})
377
+ } else if Array.length(candidates) == 0 {
378
+ NoCandidate
379
+ } else {
380
+ Ambiguous({candidates, conventional})
381
+ }
356
382
  }
357
383
  }
358
384
  }
359
385
 
360
- let resolveKeyField = (~entityName: string, schema: S.t<unknown>): option<(string, string)> =>
361
- switch classifyKeyField(~entityName, schema) {
386
+ let resolveKeyField = (~rowKeying: rowKeying, ~entityName: string, schema: S.t<unknown>): option<(
387
+ string,
388
+ string,
389
+ )> =>
390
+ switch classifyKeyField(~rowKeying, ~entityName, schema) {
362
391
  | Resolved({field, rung}) => Some((field, rung))
363
392
  | Ambiguous(_) | NoCandidate => None
364
393
  }
@@ -457,7 +486,9 @@ let deriveServerCapability = (~entityName: string, schema: S.t<unknown>): server
457
486
  // a client asked for happened client-side over one page. Its key is knowable
458
487
  // without the annotation in the common cases; take it. Pushed last, and both
459
488
  // pushes dedupe, so a declared `@id` keeps its position and this is a no-op.
460
- switch resolveKeyField(~entityName, schema) {
489
+ // Read as a state-field key for every view: a read model's lone foreign id is
490
+ // not its key, but it stays filterable, since dropping it would break the SDL.
491
+ switch resolveKeyField(~rowKeying=RowKeyedByStateField, ~entityName, schema) {
461
492
  | Some((field, _rung)) =>
462
493
  pushFilter(field, ~range=false)
463
494
  pushSort(field)
@@ -5,6 +5,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
6
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
7
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
+ import * as Semantic$Reventless from "@reventlessdev/reventless-spec/src/semantic/Semantic.res.mjs";
8
9
  import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
9
10
  import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
10
11
  import * as Api_Naming$ReventlessCore from "./Api_Naming.res.mjs";
@@ -256,7 +257,7 @@ function isKeyFieldName(name) {
256
257
  }
257
258
  }
258
259
 
259
- function classifyKeyField(entityName, schema) {
260
+ function classifyKeyField(rowKeying, entityName, schema) {
260
261
  let match = StateAnnotations$Reventless.getSpec(schema);
261
262
  let declared = match !== undefined ? match.ids[0] : undefined;
262
263
  if (declared !== undefined) {
@@ -266,15 +267,27 @@ function classifyKeyField(entityName, schema) {
266
267
  rung: "annotation"
267
268
  };
268
269
  }
269
- let candidates = Object.keys(Stdlib_Option.getOr(SchemaType$ReventlessCore.fromSuryObject("", schema), {})).filter(isKeyFieldName);
270
+ let properties;
271
+ properties = schema.type === "object" ? schema.properties : ({});
272
+ let keyOf = field => Stdlib_Option.flatMap(properties[field], Semantic$Reventless.identityKey);
273
+ let candidates = Object.keys(Stdlib_Option.getOr(SchemaType$ReventlessCore.fromSuryObject("", schema), {})).filter(field => {
274
+ if (isKeyFieldName(field)) {
275
+ return true;
276
+ } else {
277
+ return Stdlib_Option.isSome(keyOf(field));
278
+ }
279
+ });
270
280
  let singular = Api_Naming$ReventlessCore.singularize(Api_Naming$ReventlessCore.stripViewSuffix(entityName));
271
281
  let conventional = singular.slice(0, 1).toLowerCase() + singular.slice(1, singular.length) + "Id";
272
- if (candidates.includes(conventional)) {
282
+ let field = candidates.find(field => Stdlib_Option.getOr(keyOf(field), field) === conventional);
283
+ if (field !== undefined) {
273
284
  return {
274
285
  TAG: "Resolved",
275
- field: conventional,
286
+ field: field,
276
287
  rung: "convention"
277
288
  };
289
+ } else if (rowKeying === "RowKeyedByEnvelope") {
290
+ return "NoCandidate";
278
291
  } else if (candidates.length === 1) {
279
292
  return {
280
293
  TAG: "Resolved",
@@ -292,8 +305,8 @@ function classifyKeyField(entityName, schema) {
292
305
  }
293
306
  }
294
307
 
295
- function resolveKeyField(entityName, schema) {
296
- let match = classifyKeyField(entityName, schema);
308
+ function resolveKeyField(rowKeying, entityName, schema) {
309
+ let match = classifyKeyField(rowKeying, entityName, schema);
297
310
  if (typeof match !== "object" || match.TAG !== "Resolved") {
298
311
  return;
299
312
  } else {
@@ -373,7 +386,7 @@ function deriveServerCapability(entityName, schema) {
373
386
  spec.scan.forEach(name => pushFilter(name, false));
374
387
  spec.scanSort.forEach(pushSort);
375
388
  }
376
- let match = resolveKeyField(entityName, schema);
389
+ let match = resolveKeyField("RowKeyedByStateField", entityName, schema);
377
390
  if (match !== undefined) {
378
391
  let field = match[0];
379
392
  pushFilter(field, false);
@@ -45,6 +45,14 @@ let isTagged = Reventless.DcbTag.isTagged
45
45
  let isDateTime = Reventless.DateTime.isDateTime
46
46
  let isCalendarDate = Reventless.CalendarDate.isCalendarDate
47
47
 
48
+ // On the schema itself, not through `option<…>` as `Semantic.get` reads: the
49
+ // optional wrapper then reaches the `AnyOf` branch and stays `Nullable`.
50
+ let isIdentity = (schema: S.t<unknown>): bool =>
51
+ switch schema->S.Metadata.get(~id=Reventless.Semantic.semanticId) {
52
+ | Some({id}) => id === Reventless.Semantic.Id.identity
53
+ | None => false
54
+ }
55
+
48
56
  let isIdFieldName = (name: string): bool => {
49
57
  let lower = String.toLowerCase(name)
50
58
  let len = String.length(lower)
@@ -123,12 +131,14 @@ and withoutDefault = (
123
131
  }
124
132
 
125
133
  and shapeOf = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): schemaType => {
126
- // Two independent facts, checked independently. A DCB-tagged field is an
134
+ // Three independent facts, checked independently. A DCB-tagged field is an
127
135
  // entity id because it routes; a reference field is one because it points at
128
- // an entity. Neither implies the other — a `@partitionTag` field carries no
136
+ // an entity; an identity-typed field is one whatever it is called. Neither implies the other — a `@partitionTag` field carries no
129
137
  // reference, and `Reference.toWithoutDcbTag` carries no tag — so collapsing
130
138
  // these into one test would silently reclassify whichever case it dropped.
131
- if isTagged(schema) || Reventless.Reference.getTarget(schema)->Option.isSome {
139
+ if (
140
+ isTagged(schema) || Reventless.Reference.getTarget(schema)->Option.isSome || isIdentity(schema)
141
+ ) {
132
142
  EntityId
133
143
  } else {
134
144
  switch schema {
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Sury from "sury";
3
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
@@ -11,6 +12,15 @@ import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/comp
11
12
  import * as CalendarDate$Reventless from "@reventlessdev/reventless-spec/src/semantic/CalendarDate.res.mjs";
12
13
  import * as FieldDefault$Reventless from "@reventlessdev/reventless-spec/src/components/FieldDefault.res.mjs";
13
14
 
15
+ function isIdentity(schema) {
16
+ let match = Sury.$Metadata_get(schema, Semantic$Reventless.semanticId);
17
+ if (match !== undefined) {
18
+ return match.id === Semantic$Reventless.Id.identity;
19
+ } else {
20
+ return false;
21
+ }
22
+ }
23
+
14
24
  function isIdFieldName(name) {
15
25
  let lower = name.toLowerCase();
16
26
  let len = lower.length;
@@ -87,7 +97,7 @@ function withoutDefault(parentName, fieldName, schema) {
87
97
  }
88
98
 
89
99
  function shapeOf(parentName, fieldName, schema) {
90
- if (DcbTag$Reventless.isTagged(schema) || Stdlib_Option.isSome(Reference$Reventless.getTarget(schema))) {
100
+ if (DcbTag$Reventless.isTagged(schema) || Stdlib_Option.isSome(Reference$Reventless.getTarget(schema)) || isIdentity(schema)) {
91
101
  return "EntityId";
92
102
  }
93
103
  switch (schema.type) {
@@ -425,6 +435,7 @@ export {
425
435
  isTagged,
426
436
  isDateTime,
427
437
  isCalendarDate,
438
+ isIdentity,
428
439
  isIdFieldName,
429
440
  isIdsFieldName,
430
441
  semanticCompositeNames,
@@ -439,4 +450,4 @@ export {
439
450
  trailShape,
440
451
  fromSuryObject,
441
452
  }
442
- /* DcbTag-Reventless Not a pure module */
453
+ /* sury Not a pure module */
@@ -310,11 +310,15 @@ and withSemantic = (fieldSchema: JSON.t, sem: Reventless.Semantic.t): JSON.t =>
310
310
  obj->Dict.set("x-reventless-semantic-source", str("type"))
311
311
  switch sem.payload {
312
312
  | Plain => ()
313
- | ReferenceTo({entity, plugin}) =>
314
- obj->Dict.set(
315
- "x-reventless-semantic-target",
316
- jsonObject(withOptionalPlugin([("entity", str(entity))], plugin)),
317
- )
313
+ | ReferenceTo({entity, plugin, ?identity}) =>
314
+ let pairs = withOptionalPlugin([("entity", str(entity))], plugin)
315
+ let pairs = switch identity {
316
+ | Some(key) => Array.concat(pairs, [("identity", str(key))])
317
+ | None => pairs
318
+ }
319
+ obj->Dict.set("x-reventless-semantic-target", jsonObject(pairs))
320
+ | IdentityOf({key}) =>
321
+ obj->Dict.set("x-reventless-semantic-target", jsonObject([("key", str(key))]))
318
322
  | StoredIn({plugin, store}) =>
319
323
  // An absent plugin means "this plugin's own store" — omit the key rather
320
324
  // than writing an empty string, so a reader never has to tell those apart.
@@ -355,10 +355,16 @@ function withSemantic(fieldSchema, sem) {
355
355
  switch (match.TAG) {
356
356
  case "ReferenceTo" :
357
357
  let match$1 = match._0;
358
- obj["x-reventless-semantic-target"] = Object.fromEntries(withOptionalPlugin([[
358
+ let identity = match$1.identity;
359
+ let pairs = withOptionalPlugin([[
359
360
  "entity",
360
361
  match$1.entity
361
- ]], match$1.plugin));
362
+ ]], match$1.plugin);
363
+ let pairs$1 = identity !== undefined ? pairs.concat([[
364
+ "identity",
365
+ identity
366
+ ]]) : pairs;
367
+ obj["x-reventless-semantic-target"] = Object.fromEntries(pairs$1);
362
368
  break;
363
369
  case "StoredIn" :
364
370
  let match$2 = match._0;
@@ -384,6 +390,12 @@ function withSemantic(fieldSchema, sem) {
384
390
  match$3.field
385
391
  ]], match$3.plugin), "view", match$3.view), "content", match$3.content));
386
392
  break;
393
+ case "IdentityOf" :
394
+ obj["x-reventless-semantic-target"] = Object.fromEntries([[
395
+ "key",
396
+ match.key
397
+ ]]);
398
+ break;
387
399
  }
388
400
  }
389
401
  return obj;
@@ -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 = 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: 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({event, meta, recordedAt}) catch {
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 = Projection.project({event, meta: raw.meta, recordedAt: raw.recordedAt})
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)));