@cratis/chronicle 6.9.1 → 6.10.0

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 (268) hide show
  1. package/ChronicleClient.ts +52 -13
  2. package/ChronicleOptions.spec.ts +54 -0
  3. package/ChronicleOptions.ts +20 -5
  4. package/EventStore.ts +7 -1
  5. package/README.md +2 -3
  6. package/artifacts/DefaultClientArtifactsProvider.ts +4 -1
  7. package/connection/ChronicleConnection.auth.spec.ts +219 -0
  8. package/connection/ChronicleConnection.ts +70 -31
  9. package/connection/ChronicleServices.ts +1 -1
  10. package/connection/CompatibilityPreflight.ts +1 -1
  11. package/connection/RejectedChronicleCredentials.ts +10 -0
  12. package/connection/TokenProvider.spec.ts +5 -6
  13. package/connection/TokenProvider.ts +26 -17
  14. package/connection/clientVersion.spec.ts +27 -0
  15. package/connection/clientVersion.ts +9 -0
  16. package/connection/describeRequestError.spec.ts +22 -0
  17. package/connection/fetchOAuthAccessToken.ts +33 -2
  18. package/dist/ChronicleClient.d.ts +3 -0
  19. package/dist/ChronicleClient.d.ts.map +1 -1
  20. package/dist/ChronicleClient.js +55 -13
  21. package/dist/ChronicleClient.js.map +1 -1
  22. package/dist/ChronicleOptions.d.ts +5 -1
  23. package/dist/ChronicleOptions.d.ts.map +1 -1
  24. package/dist/ChronicleOptions.js +20 -5
  25. package/dist/ChronicleOptions.js.map +1 -1
  26. package/dist/EventStore.d.ts +2 -0
  27. package/dist/EventStore.d.ts.map +1 -1
  28. package/dist/EventStore.js +6 -1
  29. package/dist/EventStore.js.map +1 -1
  30. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
  31. package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -1
  32. package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
  33. package/dist/connection/ChronicleConnection.d.ts +3 -3
  34. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  35. package/dist/connection/ChronicleConnection.js +74 -26
  36. package/dist/connection/ChronicleConnection.js.map +1 -1
  37. package/dist/connection/ChronicleServices.d.ts +1 -2
  38. package/dist/connection/ChronicleServices.d.ts.map +1 -1
  39. package/dist/connection/CompatibilityPreflight.js +1 -1
  40. package/dist/connection/CompatibilityPreflight.js.map +1 -1
  41. package/dist/connection/RejectedChronicleCredentials.d.ts +5 -0
  42. package/dist/connection/RejectedChronicleCredentials.d.ts.map +1 -0
  43. package/dist/connection/RejectedChronicleCredentials.js +10 -0
  44. package/dist/connection/RejectedChronicleCredentials.js.map +1 -0
  45. package/dist/connection/TokenProvider.d.ts +8 -6
  46. package/dist/connection/TokenProvider.d.ts.map +1 -1
  47. package/dist/connection/TokenProvider.js +25 -11
  48. package/dist/connection/TokenProvider.js.map +1 -1
  49. package/dist/connection/clientVersion.d.ts +3 -0
  50. package/dist/connection/clientVersion.d.ts.map +1 -0
  51. package/dist/connection/clientVersion.js +7 -0
  52. package/dist/connection/clientVersion.js.map +1 -0
  53. package/dist/connection/fetchOAuthAccessToken.d.ts +12 -0
  54. package/dist/connection/fetchOAuthAccessToken.d.ts.map +1 -1
  55. package/dist/connection/fetchOAuthAccessToken.js +37 -2
  56. package/dist/connection/fetchOAuthAccessToken.js.map +1 -1
  57. package/dist/externalServices/ExternalServiceBuilder.d.ts +1 -1
  58. package/dist/externalServices/ExternalServiceBuilder.d.ts.map +1 -1
  59. package/dist/externalServices/ExternalServiceBuilder.js +1 -1
  60. package/dist/externalServices/ExternalServiceBuilder.js.map +1 -1
  61. package/dist/externalServices/ExternalServices.d.ts.map +1 -1
  62. package/dist/externalServices/ExternalServices.js +3 -1
  63. package/dist/externalServices/ExternalServices.js.map +1 -1
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/projections/Projections.d.ts.map +1 -1
  69. package/dist/projections/Projections.js +9 -7
  70. package/dist/projections/Projections.js.map +1 -1
  71. package/dist/projections/constantValueExpression.d.ts +3 -0
  72. package/dist/projections/constantValueExpression.d.ts.map +1 -0
  73. package/dist/projections/constantValueExpression.js +26 -0
  74. package/dist/projections/constantValueExpression.js.map +1 -0
  75. package/dist/projections/declarative/ChildrenBuilder.d.ts +2 -0
  76. package/dist/projections/declarative/ChildrenBuilder.d.ts.map +1 -1
  77. package/dist/projections/declarative/ChildrenBuilder.js +5 -0
  78. package/dist/projections/declarative/ChildrenBuilder.js.map +1 -1
  79. package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
  80. package/dist/projections/declarative/FromBuilder.js +3 -2
  81. package/dist/projections/declarative/FromBuilder.js.map +1 -1
  82. package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
  83. package/dist/projections/declarative/JoinBuilder.js +2 -1
  84. package/dist/projections/declarative/JoinBuilder.js.map +1 -1
  85. package/dist/projections/declarative/NestedBuilder.d.ts.map +1 -1
  86. package/dist/projections/declarative/NestedBuilder.js +3 -1
  87. package/dist/projections/declarative/NestedBuilder.js.map +1 -1
  88. package/dist/projections/declarative/ProjectionBuilderCore.d.ts +5 -0
  89. package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
  90. package/dist/projections/declarative/ProjectionBuilderCore.js +18 -2
  91. package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
  92. package/dist/projections/declarative/ProjectionBuilderFor.d.ts +6 -0
  93. package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
  94. package/dist/projections/declarative/ProjectionBuilderFor.js +5 -0
  95. package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
  96. package/dist/projections/declarative/SetBuilder.d.ts.map +1 -1
  97. package/dist/projections/declarative/SetBuilder.js +2 -1
  98. package/dist/projections/declarative/SetBuilder.js.map +1 -1
  99. package/dist/projections/modelBound/addFrom.d.ts.map +1 -1
  100. package/dist/projections/modelBound/addFrom.js +3 -2
  101. package/dist/projections/modelBound/addFrom.js.map +1 -1
  102. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts +20 -0
  103. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
  104. package/dist/projections/modelBound/childrenAndNestedBuilder.js +14 -11
  105. package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
  106. package/dist/projections/modelBound/childrenFrom.d.ts +4 -2
  107. package/dist/projections/modelBound/childrenFrom.d.ts.map +1 -1
  108. package/dist/projections/modelBound/childrenFrom.js +8 -4
  109. package/dist/projections/modelBound/childrenFrom.js.map +1 -1
  110. package/dist/projections/modelBound/clearWith.d.ts.map +1 -1
  111. package/dist/projections/modelBound/clearWith.js +3 -2
  112. package/dist/projections/modelBound/clearWith.js.map +1 -1
  113. package/dist/projections/modelBound/count.d.ts.map +1 -1
  114. package/dist/projections/modelBound/count.js +3 -2
  115. package/dist/projections/modelBound/count.js.map +1 -1
  116. package/dist/projections/modelBound/decrement.d.ts.map +1 -1
  117. package/dist/projections/modelBound/decrement.js +3 -2
  118. package/dist/projections/modelBound/decrement.js.map +1 -1
  119. package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
  120. package/dist/projections/modelBound/fromAll.js +3 -2
  121. package/dist/projections/modelBound/fromAll.js.map +1 -1
  122. package/dist/projections/modelBound/fromEvery.d.ts.map +1 -1
  123. package/dist/projections/modelBound/fromEvery.js +3 -2
  124. package/dist/projections/modelBound/fromEvery.js.map +1 -1
  125. package/dist/projections/modelBound/increment.d.ts.map +1 -1
  126. package/dist/projections/modelBound/increment.js +3 -2
  127. package/dist/projections/modelBound/increment.js.map +1 -1
  128. package/dist/projections/modelBound/join.d.ts.map +1 -1
  129. package/dist/projections/modelBound/join.js +3 -2
  130. package/dist/projections/modelBound/join.js.map +1 -1
  131. package/dist/projections/modelBound/nested.d.ts.map +1 -1
  132. package/dist/projections/modelBound/nested.js +3 -2
  133. package/dist/projections/modelBound/nested.js.map +1 -1
  134. package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -1
  135. package/dist/projections/modelBound/noAutoMap.js +3 -2
  136. package/dist/projections/modelBound/noAutoMap.js.map +1 -1
  137. package/dist/projections/modelBound/removedWith.d.ts.map +1 -1
  138. package/dist/projections/modelBound/removedWith.js +3 -2
  139. package/dist/projections/modelBound/removedWith.js.map +1 -1
  140. package/dist/projections/modelBound/removedWithJoin.d.ts.map +1 -1
  141. package/dist/projections/modelBound/removedWithJoin.js +3 -2
  142. package/dist/projections/modelBound/removedWithJoin.js.map +1 -1
  143. package/dist/projections/modelBound/setFrom.d.ts.map +1 -1
  144. package/dist/projections/modelBound/setFrom.js +3 -2
  145. package/dist/projections/modelBound/setFrom.js.map +1 -1
  146. package/dist/projections/modelBound/setFromContext.d.ts.map +1 -1
  147. package/dist/projections/modelBound/setFromContext.js +3 -2
  148. package/dist/projections/modelBound/setFromContext.js.map +1 -1
  149. package/dist/projections/modelBound/setValue.d.ts.map +1 -1
  150. package/dist/projections/modelBound/setValue.js +3 -2
  151. package/dist/projections/modelBound/setValue.js.map +1 -1
  152. package/dist/projections/modelBound/subtractFrom.d.ts.map +1 -1
  153. package/dist/projections/modelBound/subtractFrom.js +3 -2
  154. package/dist/projections/modelBound/subtractFrom.js.map +1 -1
  155. package/dist/projections/notSetPropertyPath.d.ts +3 -0
  156. package/dist/projections/notSetPropertyPath.d.ts.map +1 -0
  157. package/dist/projections/notSetPropertyPath.js +5 -0
  158. package/dist/projections/notSetPropertyPath.js.map +1 -0
  159. package/dist/reactors/Reactors.d.ts +4 -0
  160. package/dist/reactors/Reactors.d.ts.map +1 -1
  161. package/dist/reactors/Reactors.js +24 -6
  162. package/dist/reactors/Reactors.js.map +1 -1
  163. package/dist/reactors/reactor.d.ts +2 -2
  164. package/dist/reactors/reactor.js +2 -2
  165. package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
  166. package/dist/readModels/MaterializedReadModels.js +2 -4
  167. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  168. package/dist/readModels/ReadModels.d.ts +2 -1
  169. package/dist/readModels/ReadModels.d.ts.map +1 -1
  170. package/dist/readModels/ReadModels.js +16 -10
  171. package/dist/readModels/ReadModels.js.map +1 -1
  172. package/dist/readModels/deserializeReadModel.d.ts +5 -0
  173. package/dist/readModels/deserializeReadModel.d.ts.map +1 -0
  174. package/dist/readModels/deserializeReadModel.js +84 -0
  175. package/dist/readModels/deserializeReadModel.js.map +1 -0
  176. package/dist/readModels/rootReadModelTypes.d.ts +5 -0
  177. package/dist/readModels/rootReadModelTypes.d.ts.map +1 -0
  178. package/dist/readModels/rootReadModelTypes.js +69 -0
  179. package/dist/readModels/rootReadModelTypes.js.map +1 -0
  180. package/dist/reducers/Reducers.d.ts +4 -0
  181. package/dist/reducers/Reducers.d.ts.map +1 -1
  182. package/dist/reducers/Reducers.js +24 -6
  183. package/dist/reducers/Reducers.js.map +1 -1
  184. package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
  185. package/dist/schemas/JsonSchemaGenerator.js +4 -4
  186. package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
  187. package/dist/tsconfig.tsbuildinfo +1 -1
  188. package/dist/types/TypeDiscoverer.d.ts +8 -1
  189. package/dist/types/TypeDiscoverer.d.ts.map +1 -1
  190. package/dist/types/TypeDiscoverer.js +25 -5
  191. package/dist/types/TypeDiscoverer.js.map +1 -1
  192. package/dist/types/TypeIntrospector.d.ts.map +1 -1
  193. package/dist/types/TypeIntrospector.js +3 -2
  194. package/dist/types/TypeIntrospector.js.map +1 -1
  195. package/dist/types/conceptValueType.d.ts +4 -0
  196. package/dist/types/conceptValueType.d.ts.map +1 -0
  197. package/dist/types/conceptValueType.js +10 -0
  198. package/dist/types/conceptValueType.js.map +1 -0
  199. package/dist/types/modelBoundProperty.d.ts +6 -0
  200. package/dist/types/modelBoundProperty.d.ts.map +1 -0
  201. package/dist/types/modelBoundProperty.js +53 -0
  202. package/dist/types/modelBoundProperty.js.map +1 -0
  203. package/externalServices/ExternalServiceBuilder.ts +4 -4
  204. package/externalServices/ExternalServices.spec.ts +87 -0
  205. package/externalServices/ExternalServices.ts +3 -1
  206. package/for_ChronicleClient/connectionAuthentication.spec.ts +67 -0
  207. package/index.ts +1 -0
  208. package/observation/observationDisposal.spec.ts +74 -0
  209. package/package.json +2 -1
  210. package/projections/Projections.childrenAndNested.spec.ts +2 -0
  211. package/projections/Projections.modelBoundCompleteness.spec.ts +47 -0
  212. package/projections/Projections.ts +9 -7
  213. package/projections/constantValueExpression.spec.ts +31 -0
  214. package/projections/constantValueExpression.ts +28 -0
  215. package/projections/declarative/ChildrenBuilder.spec.ts +46 -1
  216. package/projections/declarative/ChildrenBuilder.ts +6 -0
  217. package/projections/declarative/FromBuilder.ts +3 -2
  218. package/projections/declarative/JoinBuilder.ts +2 -1
  219. package/projections/declarative/NestedBuilder.ts +3 -1
  220. package/projections/declarative/ProjectionBuilderCore.ts +20 -2
  221. package/projections/declarative/ProjectionBuilderFor.spec.ts +61 -4
  222. package/projections/declarative/ProjectionBuilderFor.ts +7 -0
  223. package/projections/declarative/SetBuilder.ts +2 -1
  224. package/projections/modelBound/addFrom.ts +3 -2
  225. package/projections/modelBound/childrenAndNestedBuilder.ts +14 -11
  226. package/projections/modelBound/childrenFrom.ts +11 -5
  227. package/projections/modelBound/clearWith.ts +3 -2
  228. package/projections/modelBound/count.ts +3 -2
  229. package/projections/modelBound/decorators.standard.spec.ts +45 -0
  230. package/projections/modelBound/decrement.ts +3 -2
  231. package/projections/modelBound/fromAll.ts +3 -2
  232. package/projections/modelBound/fromEvery.ts +3 -2
  233. package/projections/modelBound/increment.ts +3 -2
  234. package/projections/modelBound/join.ts +3 -2
  235. package/projections/modelBound/nested.ts +3 -2
  236. package/projections/modelBound/noAutoMap.ts +3 -2
  237. package/projections/modelBound/removedWith.ts +3 -2
  238. package/projections/modelBound/removedWithJoin.ts +3 -2
  239. package/projections/modelBound/setFrom.ts +3 -2
  240. package/projections/modelBound/setFromContext.ts +3 -2
  241. package/projections/modelBound/setValue.ts +3 -2
  242. package/projections/modelBound/subtractFrom.ts +3 -2
  243. package/projections/notSetPropertyPath.ts +5 -0
  244. package/reactors/Reactors.ts +22 -7
  245. package/reactors/reactor.ts +2 -2
  246. package/readModels/MaterializedReadModels.spec.ts +22 -0
  247. package/readModels/MaterializedReadModels.ts +2 -4
  248. package/readModels/ReadModels.spec.ts +58 -1
  249. package/readModels/ReadModels.ts +17 -11
  250. package/readModels/conceptValueType.spec.ts +20 -0
  251. package/readModels/deserializeReadModel.spec.ts +84 -0
  252. package/readModels/deserializeReadModel.ts +82 -0
  253. package/readModels/discovery.spec.ts +27 -0
  254. package/readModels/rootReadModelTypes.spec.ts +115 -0
  255. package/readModels/rootReadModelTypes.ts +65 -0
  256. package/reducers/Reducers.ts +22 -7
  257. package/schemas/JsonSchemaGenerator.ts +4 -4
  258. package/schemas/legacyDesignMetadata.spec.ts +24 -2
  259. package/types/TypeDiscoverer.spec.ts +57 -0
  260. package/types/TypeDiscoverer.ts +27 -6
  261. package/types/TypeIntrospector.ts +3 -2
  262. package/types/conceptValueType.ts +11 -0
  263. package/types/modelBoundProperty.ts +53 -0
  264. package/dist/externalServices/ExternalServicesContracts.d.ts +0 -137
  265. package/dist/externalServices/ExternalServicesContracts.d.ts.map +0 -1
  266. package/dist/externalServices/ExternalServicesContracts.js +0 -654
  267. package/dist/externalServices/ExternalServicesContracts.js.map +0 -1
  268. package/externalServices/ExternalServicesContracts.ts +0 -709
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** Converts a constant to the projection expression understood by the Chronicle kernel. */
5
+ export function constantValueExpression(value: unknown): string {
6
+ if (value === null || value === undefined) {
7
+ return '$null';
8
+ }
9
+
10
+ if (value instanceof Date) {
11
+ return `$value(${value.toISOString()})`;
12
+ }
13
+
14
+ if (typeof value === 'object' && 'value' in value) {
15
+ return constantValueExpression(value.value);
16
+ }
17
+
18
+ const text = String(value);
19
+ if (text === '$null') return text;
20
+ const wrapped = text.startsWith('$value(');
21
+ const content = wrapped && text.endsWith(')') ? text.slice(7, -1) : text;
22
+ // ValueExpressionResolver accepts only word characters, spaces, and ._/:*+-.
23
+ // .NET's \w also accepts Unicode letters, non-spacing marks, decimal digits and connector punctuation.
24
+ if ((wrapped && !text.endsWith(')')) || !/^[\p{L}\p{Mn}\p{Nd}\p{Pc} ._/:*+-]*$/u.test(content)) {
25
+ throw new Error(`Constant value '${text}' contains characters unsupported by the kernel's $value expression.`);
26
+ }
27
+ return wrapped ? text : `$value(${text})`;
28
+ }
@@ -36,10 +36,12 @@ eventType()(WholeLineReceived);
36
36
  class OrderLine {
37
37
  productId!: string;
38
38
  quantity!: number;
39
+ summary!: OrderSummary | undefined;
39
40
  }
40
41
 
41
42
  class OrderSummary {
42
43
  total!: number;
44
+ details!: OrderSummary | undefined;
43
45
  }
44
46
 
45
47
  class Order {
@@ -59,6 +61,8 @@ interface ChildrenDefinition {
59
61
  From: FromRecord[];
60
62
  RemovedWith: Array<{ Key: { Id: string } }>;
61
63
  FromEventProperty?: { Event: { Id: string } | undefined; PropertyExpression: string };
64
+ Nested: Record<string, ChildrenDefinition>;
65
+ Join: Array<{ Value: { On: string } }>;
62
66
  }
63
67
 
64
68
  interface BuiltDefinition {
@@ -68,6 +72,21 @@ interface BuiltDefinition {
68
72
  }
69
73
 
70
74
  describe('ChildrenBuilder and NestedBuilder', () => {
75
+ it('resolves a child join after identifiedBy is configured', () => {
76
+ const builder = new ProjectionBuilderFor<Order>();
77
+ builder.children<OrderLine>(order => order.lines, child => child
78
+ .join(LineAdded, join => join.set(line => line.quantity).to(event => event.quantity))
79
+ .identifiedBy(line => line.productId));
80
+ const definition = builder.build('order', 'Order') as unknown as BuiltDefinition;
81
+ expect(definition.Children.lines.Join[0].Value.On).toBe('productId');
82
+ });
83
+
84
+ it('rejects a root join without on at build time', () => {
85
+ const builder = new ProjectionBuilderFor<Order>();
86
+ expect(() => builder.join(LineAdded)).not.toThrow();
87
+ expect(() => builder.build('order', 'Order')).toThrow("A join with event 'LineAdded' requires an on property.");
88
+ });
89
+
71
90
  describe('when using children on the top-level builder', () => {
72
91
  it('should produce a children definition keyed by the target property', () => {
73
92
  const builder = new ProjectionBuilderFor<Order>();
@@ -94,12 +113,26 @@ describe('ChildrenBuilder and NestedBuilder', () => {
94
113
  const definition = builder.build('order', 'Order') as unknown as BuiltDefinition;
95
114
  const summaryDefinition = definition.Nested.summary;
96
115
 
116
+ expect(summaryDefinition.IdentifiedBy).toBe('*NotSet*');
97
117
  const fromEntry = summaryDefinition.From.find(candidate => candidate.Key.Id === 'SummaryUpdated')!;
98
118
  expect(fromEntry.Value.Properties.total).toBe('total');
99
119
  expect(summaryDefinition.RemovedWith[0].Key.Id).toBe('SummaryCleared');
100
120
  });
101
121
  });
102
122
 
123
+ describe('when nesting within a child or nested definition', () => {
124
+ it('should mark every nested definition as not identified by a property', () => {
125
+ const builder = new ProjectionBuilderFor<Order>();
126
+ builder.children<OrderLine>(order => order.lines, child => child
127
+ .nested<OrderSummary>(line => line.summary, nested => nested
128
+ .nested<OrderSummary>(summary => summary.details, () => {})));
129
+
130
+ const definition = builder.build('order', 'Order') as unknown as BuiltDefinition;
131
+ expect(definition.Children.lines.Nested.summary.IdentifiedBy).toBe('*NotSet*');
132
+ expect(definition.Children.lines.Nested.summary.Nested.details.IdentifiedBy).toBe('*NotSet*');
133
+ });
134
+ });
135
+
103
136
  describe('when using addChild with a builder callback', () => {
104
137
  it('should produce a children definition keyed by identifiedBy and usingKey', () => {
105
138
  const builder = new ProjectionBuilderFor<Order>();
@@ -132,7 +165,7 @@ describe('ChildrenBuilder and NestedBuilder', () => {
132
165
  describe('when using addChild on a join builder', () => {
133
166
  it('should produce a children definition from the join clause', () => {
134
167
  const builder = new ProjectionBuilderFor<Order>();
135
- builder.join(WholeLineReceived, join => join.addChild<OrderLine>(order => order.lines, child => child
168
+ builder.join(WholeLineReceived, join => join.on(order => order.id).addChild<OrderLine>(order => order.lines, child => child
136
169
  .identifiedBy(line => line.productId)));
137
170
 
138
171
  const definition = builder.build('order', 'Order') as unknown as BuiltDefinition;
@@ -143,6 +176,18 @@ describe('ChildrenBuilder and NestedBuilder', () => {
143
176
  });
144
177
  });
145
178
 
179
+ describe('when joining from a child projection', () => {
180
+ it('should use the child identifier when on is omitted', () => {
181
+ const builder = new ProjectionBuilderFor<Order>();
182
+ builder.children<OrderLine>(order => order.lines, child => child
183
+ .identifiedBy(line => line.productId)
184
+ .join(LineAdded, join => join.set(line => line.quantity).to(event => event.quantity)));
185
+
186
+ const definition = builder.build('order', 'Order') as unknown as BuiltDefinition;
187
+ expect(definition.Children.lines.Join[0].Value.On).toBe('productId');
188
+ });
189
+ });
190
+
146
191
  describe('when using setThisValue on a from builder', () => {
147
192
  it('should map the whole event property onto the $this well-known expression', () => {
148
193
  const builder = new ProjectionBuilderFor<Order>();
@@ -67,11 +67,17 @@ export class ChildrenBuilder<TParentReadModel, TChildReadModel>
67
67
  return this;
68
68
  }
69
69
 
70
+ /** @inheritdoc */
71
+ protected defaultJoinOn(): string | undefined {
72
+ return this._identifiedBy;
73
+ }
74
+
70
75
  /**
71
76
  * Builds the {@link ChildrenDefinitionLike} representing this children collection.
72
77
  * @returns The accumulated children definition.
73
78
  */
74
79
  buildDefinition(): ChildrenDefinitionLike {
80
+ this.resolveJoins();
75
81
  return {
76
82
  IdentifiedBy: this._identifiedBy ?? '$eventSourceId',
77
83
  From: this._from,
@@ -2,6 +2,7 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
5
+ import { constantValueExpression } from '../constantValueExpression.js';
5
6
  import { AddBuilder } from './AddBuilder.js';
6
7
  import { AddChildBuilder, ChildAdditionEntry } from './AddChildBuilder.js';
7
8
  import { CompositeKeyBuilder } from './CompositeKeyBuilder.js';
@@ -98,13 +99,13 @@ export class FromBuilder<TReadModel, TEvent> implements IFromBuilder<TReadModel,
98
99
 
99
100
  /** @inheritdoc */
100
101
  usingConstantKey(value: string): this {
101
- this.entry.key = value;
102
+ this.entry.key = constantValueExpression(value);
102
103
  return this;
103
104
  }
104
105
 
105
106
  /** @inheritdoc */
106
107
  usingConstantParentKey(value: string): this {
107
- this.entry.parentKey = value;
108
+ this.entry.parentKey = constantValueExpression(value);
108
109
  return this;
109
110
  }
110
111
 
@@ -2,6 +2,7 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
5
+ import { constantValueExpression } from '../constantValueExpression.js';
5
6
  import { AddBuilder } from './AddBuilder.js';
6
7
  import { AddChildBuilder, ChildAdditionEntry } from './AddChildBuilder.js';
7
8
  import { CompositeKeyBuilder } from './CompositeKeyBuilder.js';
@@ -106,7 +107,7 @@ export class JoinBuilder<TReadModel, TEvent> implements IJoinBuilder<TReadModel,
106
107
 
107
108
  /** @inheritdoc */
108
109
  usingConstantKey(value: string): this {
109
- this.entry.key = value;
110
+ this.entry.key = constantValueExpression(value);
110
111
  return this;
111
112
  }
112
113
 
@@ -6,6 +6,7 @@ import { ChildrenDefinitionLike, ProjectionBuilderCore } from './ProjectionBuild
6
6
  import { IChildrenBuilder } from './IChildrenBuilder.js';
7
7
  import { INestedBuilder } from './INestedBuilder.js';
8
8
  import { ChildrenBuilder } from './ChildrenBuilder.js';
9
+ import { notSetPropertyPath } from '../notSetPropertyPath.js';
9
10
 
10
11
  /**
11
12
  * Concrete implementation of {@link INestedBuilder}, building the `ChildrenDefinition` for a
@@ -61,8 +62,9 @@ export class NestedBuilder<TParentReadModel, TNestedReadModel>
61
62
  * @returns The accumulated nested definition.
62
63
  */
63
64
  buildDefinition(): ChildrenDefinitionLike {
65
+ this.resolveJoins();
64
66
  return {
65
- IdentifiedBy: '',
67
+ IdentifiedBy: notSetPropertyPath,
66
68
  From: this._from,
67
69
  Join: this._join,
68
70
  Children: this._children,
@@ -73,6 +73,7 @@ export abstract class ProjectionBuilderCore<TReadModel, TBuilder> {
73
73
  protected _initialState: string = '{}';
74
74
  protected readonly _from: FromRecord[] = [];
75
75
  protected readonly _join: JoinRecord[] = [];
76
+ private readonly _joinEventNames = new Map<JoinRecord, string>();
76
77
  protected readonly _removedWith: RemovedWithRecord[] = [];
77
78
  protected readonly _removedWithJoin: RemovedWithJoinRecord[] = [];
78
79
  protected readonly _children: Record<string, ChildrenDefinitionLike> = {};
@@ -129,14 +130,16 @@ export abstract class ProjectionBuilderCore<TReadModel, TBuilder> {
129
130
  const contractType = this.toContractEventType(eventType);
130
131
  const joinBuilder = new JoinBuilder<TReadModel, TEvent>();
131
132
  builderCallback?.(joinBuilder);
132
- this._join.push({
133
+ const record: JoinRecord = {
133
134
  Key: contractType,
134
135
  Value: {
135
136
  On: joinBuilder.entry.on,
136
137
  Properties: joinBuilder.entry.properties,
137
138
  Key: joinBuilder.entry.key
138
139
  }
139
- });
140
+ };
141
+ this._join.push(record);
142
+ this._joinEventNames.set(record, eventType.name);
140
143
  this.mergeChildAdditions(contractType, joinBuilder.entry.children);
141
144
  return this as unknown as TBuilder;
142
145
  }
@@ -191,6 +194,21 @@ export abstract class ProjectionBuilderCore<TReadModel, TBuilder> {
191
194
  return this as unknown as TBuilder;
192
195
  }
193
196
 
197
+ /** Resolves join defaults once the entire projection (including identifiedBy) is configured. */
198
+ protected resolveJoins(): void {
199
+ for (const record of this._join) {
200
+ record.Value.On ||= this.defaultJoinOn() ?? '';
201
+ if (!record.Value.On) {
202
+ throw new Error(`A join with event '${this._joinEventNames.get(record)}' requires an on property.`);
203
+ }
204
+ }
205
+ }
206
+
207
+ /** Returns a default join-on path for children, if one is available. */
208
+ protected defaultJoinOn(): string | undefined {
209
+ return undefined;
210
+ }
211
+
194
212
  /**
195
213
  * Creates an empty {@link ChildrenDefinitionLike} accumulator for a children/nested entry.
196
214
  * @returns A fresh, empty children definition.
@@ -28,6 +28,10 @@ class Inventory {
28
28
  removedTotal!: number;
29
29
  thingsHappenedCount!: number;
30
30
  countsByEventType!: Record<string, number>;
31
+ active!: boolean;
32
+ status!: string;
33
+ started!: Date;
34
+ cleared!: string | null;
31
35
  incrementsByEventType!: Record<string, number>;
32
36
  decrementsByEventType!: Record<string, number>;
33
37
  }
@@ -81,14 +85,48 @@ describe('ProjectionBuilderFor', () => {
81
85
  const entry = definition.From.find(candidate => candidate.Key.Id === 'ThingHappened')!;
82
86
 
83
87
  expect(entry.Value.Properties.thingsHappenedCount).toBe('$count');
84
- expect(entry.Value.Key).toBe('singleton');
88
+ expect(entry.Value.Key).toBe('$value(singleton)');
89
+ });
90
+ });
91
+
92
+ describe('when setting constants on a from builder', () => {
93
+ it('should encode constants as value expressions and null as a clear', () => {
94
+ const builder = new ProjectionBuilderFor<Inventory>();
95
+ builder.from(ThingHappened, from => {
96
+ from.set(model => model.active).toValue(true);
97
+ from.set(model => model.status).toValue('on-loan');
98
+ from.set(model => model.total).toValue(2.5);
99
+ from.set(model => model.started).toValue(new Date('2025-01-02T03:04:05.006Z'));
100
+ from.set(model => model.cleared).toValue(null);
101
+ from.set(model => model.id).toValue({ value: 'wrapped' });
102
+ from.setThisValue().toValue('whole');
103
+ });
104
+
105
+ const definition = builder.build('inventory', 'Inventory') as unknown as { From: FromRecord[] };
106
+ expect(definition.From[0].Value.Properties).toMatchObject({
107
+ active: '$value(true)',
108
+ status: '$value(on-loan)',
109
+ total: '$value(2.5)',
110
+ started: '$value(2025-01-02T03:04:05.006Z)',
111
+ cleared: '$null',
112
+ id: '$value(wrapped)',
113
+ $this: '$value(whole)'
114
+ });
115
+ });
116
+
117
+ it('should encode constant keys and parent keys as expressions', () => {
118
+ const builder = new ProjectionBuilderFor<Inventory>();
119
+ builder.from(ThingHappened, from => from.usingConstantKey('singleton').usingConstantParentKey('parent'));
120
+ const definition = builder.build('inventory', 'Inventory') as unknown as { From: Array<{ Value: { Key: string; ParentKey: string } }> };
121
+ expect(definition.From[0].Value.Key).toBe('$value(singleton)');
122
+ expect(definition.From[0].Value.ParentKey).toBe('$value(parent)');
85
123
  });
86
124
  });
87
125
 
88
126
  describe('when using add on a join builder', () => {
89
127
  it('should produce a $add expression targeting the event property', () => {
90
128
  const builder = new ProjectionBuilderFor<Inventory>();
91
- builder.join(ItemAdded, join => join.add(model => model.total).with(event => event.quantity));
129
+ builder.join(ItemAdded, join => join.on(model => model.id).add(model => model.total).with(event => event.quantity));
92
130
 
93
131
  const definition = builder.build('inventory', 'Inventory') as unknown as { Join: JoinRecord[] };
94
132
  const entry = definition.Join.find(candidate => candidate.Key.Id === 'ItemAdded')!;
@@ -97,10 +135,20 @@ describe('ProjectionBuilderFor', () => {
97
135
  });
98
136
  });
99
137
 
138
+ describe('when setting a constant on a join builder', () => {
139
+ it('should send a value expression for the property and key', () => {
140
+ const builder = new ProjectionBuilderFor<Inventory>();
141
+ builder.join(ItemAdded, join => join.on(model => model.id).usingConstantKey('singleton').set(model => model.status).toValue('on-loan'));
142
+ const definition = builder.build('inventory', 'Inventory') as unknown as { Join: Array<{ Value: { Key: string; Properties: Record<string, string> } }> };
143
+ expect(definition.Join[0].Value.Key).toBe('$value(singleton)');
144
+ expect(definition.Join[0].Value.Properties.status).toBe('$value(on-loan)');
145
+ });
146
+ });
147
+
100
148
  describe('when using subtract on a join builder', () => {
101
149
  it('should produce a $subtract expression targeting the event property', () => {
102
150
  const builder = new ProjectionBuilderFor<Inventory>();
103
- builder.join(ItemRemoved, join => join.subtract(model => model.removedTotal).with(event => event.quantity));
151
+ builder.join(ItemRemoved, join => join.on(model => model.id).subtract(model => model.removedTotal).with(event => event.quantity));
104
152
 
105
153
  const definition = builder.build('inventory', 'Inventory') as unknown as { Join: JoinRecord[] };
106
154
  const entry = definition.Join.find(candidate => candidate.Key.Id === 'ItemRemoved')!;
@@ -112,7 +160,7 @@ describe('ProjectionBuilderFor', () => {
112
160
  describe('when using count on a join builder', () => {
113
161
  it('should produce a $count expression', () => {
114
162
  const builder = new ProjectionBuilderFor<Inventory>();
115
- builder.join(ThingHappened, join => join.count(model => model.thingsHappenedCount));
163
+ builder.join(ThingHappened, join => join.on(model => model.id).count(model => model.thingsHappenedCount));
116
164
 
117
165
  const definition = builder.build('inventory', 'Inventory') as unknown as { Join: JoinRecord[] };
118
166
  const entry = definition.Join.find(candidate => candidate.Key.Id === 'ThingHappened')!;
@@ -121,6 +169,15 @@ describe('ProjectionBuilderFor', () => {
121
169
  });
122
170
  });
123
171
 
172
+ describe('when joining without an on property', () => {
173
+ it('should reject a definition that the kernel cannot convert', () => {
174
+ const builder = new ProjectionBuilderFor<Inventory>();
175
+ builder.join(ThingHappened, join => join.count(model => model.thingsHappenedCount));
176
+ expect(() => builder.build('inventory', 'Inventory'))
177
+ .toThrow("A join with event 'ThingHappened' requires an on property.");
178
+ });
179
+ });
180
+
124
181
  describe('when using count with event context property on fromEvery', () => {
125
182
  it('should produce a $count expression with dictionary key from event context', () => {
126
183
  const builder = new ProjectionBuilderFor<Inventory>();
@@ -4,6 +4,7 @@
4
4
  import { AutoMap } from '@cratis/chronicle.contracts';
5
5
  import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
6
6
  import { EventSequenceId } from '../../eventSequences/EventSequenceId.js';
7
+ import type { ChildrenDefinitionLike } from '../modelBound/childrenAndNestedBuilder.js';
7
8
  import { ChildrenBuilder } from './ChildrenBuilder.js';
8
9
  import { IChildrenBuilder } from './IChildrenBuilder.js';
9
10
  import { INestedBuilder } from './INestedBuilder.js';
@@ -120,6 +121,11 @@ export class ProjectionBuilderFor<TReadModel> extends ProjectionBuilderCore<TRea
120
121
  return this;
121
122
  }
122
123
 
124
+ /** Names of children and nested members configured by declarative builders. */
125
+ getSubobjectDefinitions(): { children: Record<string, ChildrenDefinitionLike>; nested: Record<string, ChildrenDefinitionLike> } {
126
+ return { children: this._children, nested: this._nested };
127
+ }
128
+
123
129
  /**
124
130
  * Builds the contract-compatible projection definition payload.
125
131
  * @param identifier - The projection identifier.
@@ -127,6 +133,7 @@ export class ProjectionBuilderFor<TReadModel> extends ProjectionBuilderCore<TRea
127
133
  * @returns The projection definition object ready to send to the kernel.
128
134
  */
129
135
  build(identifier: string, readModelName: string): Record<string, unknown> {
136
+ this.resolveJoins();
130
137
  const definition: Record<string, unknown> = {
131
138
  EventSequenceId: this._eventSequenceId,
132
139
  Identifier: identifier,
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
5
5
  import { ISetBuilder } from './ISetBuilder.js';
6
+ import { constantValueExpression } from '../constantValueExpression.js';
6
7
 
7
8
  /**
8
9
  * Concrete implementation of {@link ISetBuilder} that records the property mapping
@@ -28,7 +29,7 @@ export class SetBuilder<TEvent, TParentBuilder> implements ISetBuilder<TEvent, T
28
29
 
29
30
  /** @inheritdoc */
30
31
  toValue<TProperty>(value: TProperty): TParentBuilder {
31
- this._setProperty(this._readModelProperty, JSON.stringify(value));
32
+ this._setProperty(this._readModelProperty, constantValueExpression(value));
32
33
  return this._parent;
33
34
  }
34
35
 
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the addFrom property decorator. */
9
10
  export interface AddFromMetadata {
@@ -22,7 +23,7 @@ const METADATA_KEY = 'chronicle:projection:addFrom';
22
23
  * @returns A property decorator.
23
24
  */
24
25
  export function addFrom(eventType: Function, eventPropertyName?: string): ChroniclePropertyDecorator {
25
- return decorateProperty((target: object, propertyKey: string | symbol) => {
26
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
26
27
  const key = propertyKey.toString();
27
28
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
28
29
  const existing: AddFromMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];
@@ -5,6 +5,8 @@ import 'reflect-metadata';
5
5
  import { AutoMap } from '@cratis/chronicle.contracts';
6
6
  import { Constructor, Fields } from '@cratis/fundamentals';
7
7
  import { TypeIntrospector } from '../../types/index.js';
8
+ import { constantValueExpression } from '../constantValueExpression.js';
9
+ import { notSetPropertyPath } from '../notSetPropertyPath.js';
8
10
  import { getEventTypeFor } from '../../events/eventTypeDecorator.js';
9
11
  import { getAddFromMetadata } from './addFrom.js';
10
12
  import { ChildrenFromMetadata, getChildrenFromMetadata } from './childrenFrom.js';
@@ -110,7 +112,7 @@ export function applyPropertyMappings(prototype: object, property: string, fromB
110
112
 
111
113
  for (const mapping of getSetValueMetadata(prototype, property)) {
112
114
  const entry = ensureFromEntry(fromByEventType, mapping.eventType);
113
- entry.Value.Properties[property] = JSON.stringify(mapping.value);
115
+ entry.Value.Properties[property] = constantValueExpression(mapping.value);
114
116
  }
115
117
 
116
118
  for (const mapping of getAddFromMetadata(prototype, property)) {
@@ -127,7 +129,7 @@ export function applyPropertyMappings(prototype: object, property: string, fromB
127
129
  const entry = ensureFromEntry(fromByEventType, mapping.eventType);
128
130
  entry.Value.Properties[property] = '$increment';
129
131
  if (mapping.constantKey) {
130
- entry.Value.Key = `$value(${mapping.constantKey})`;
132
+ entry.Value.Key = constantValueExpression(mapping.constantKey);
131
133
  }
132
134
  }
133
135
 
@@ -135,7 +137,7 @@ export function applyPropertyMappings(prototype: object, property: string, fromB
135
137
  const entry = ensureFromEntry(fromByEventType, mapping.eventType);
136
138
  entry.Value.Properties[property] = '$decrement';
137
139
  if (mapping.constantKey) {
138
- entry.Value.Key = `$value(${mapping.constantKey})`;
140
+ entry.Value.Key = constantValueExpression(mapping.constantKey);
139
141
  }
140
142
  }
141
143
 
@@ -143,7 +145,7 @@ export function applyPropertyMappings(prototype: object, property: string, fromB
143
145
  const entry = ensureFromEntry(fromByEventType, mapping.eventType);
144
146
  entry.Value.Properties[property] = '$count';
145
147
  if (mapping.constantKey) {
146
- entry.Value.Key = `$value(${mapping.constantKey})`;
148
+ entry.Value.Key = constantValueExpression(mapping.constantKey);
147
149
  }
148
150
  }
149
151
  }
@@ -164,17 +166,17 @@ function createEmptyChildrenDefinition(): ChildrenDefinitionLike {
164
166
 
165
167
  /**
166
168
  * Resolves the element type of a `@childrenFrom` collection property. TypeScript erases
167
- * generic type arguments at runtime, so the element type can only be recovered when the
168
- * property was declared with `@field(Array, { genericArguments: [ItemType] })` - without it,
169
+ * generic type arguments at runtime, so the element type must be supplied by @childrenFrom
170
+ * or `@field(Array, { genericArguments: [ItemType] })` - without it,
169
171
  * the children definition still registers correctly (structural Key/ParentKey wiring plus
170
172
  * AutoMap), it just cannot also translate the child type's own decorators.
171
173
  * @param type - The declaring class constructor.
172
174
  * @param property - The property name.
173
175
  * @returns The child element type constructor, or undefined when it cannot be resolved.
174
176
  */
175
- function resolveChildElementType(type: Function, property: string): Function | undefined {
177
+ export function resolveChildElementType(type: Function, property: string): Function | undefined {
176
178
  const field = Fields.getFieldsForType(type as Constructor).find(candidate => candidate.name === property);
177
- return field?.genericArguments?.[0];
179
+ return getChildrenFromMetadata(type.prototype, property).find(metadata => metadata.childType)?.childType ?? field?.genericArguments?.[0];
178
180
  }
179
181
 
180
182
  /**
@@ -185,7 +187,7 @@ function resolveChildElementType(type: Function, property: string): Function | u
185
187
  * @param property - The property name.
186
188
  * @returns The nested type constructor, or undefined when it cannot be resolved.
187
189
  */
188
- function resolveNestedType(type: Function, property: string): Function | undefined {
190
+ export function resolveNestedType(type: Function, property: string): Function | undefined {
189
191
  const field = Fields.getFieldsForType(type as Constructor).find(candidate => candidate.name === property);
190
192
  if (field?.type && field.type !== Object && field.type !== Array) {
191
193
  return field.type;
@@ -196,7 +198,8 @@ function resolveNestedType(type: Function, property: string): Function | undefin
196
198
  return designType;
197
199
  }
198
200
 
199
- return undefined;
201
+ const inferred = TypeIntrospector.getMembers(type).get(property);
202
+ return inferred && inferred !== Object && inferred !== Array ? inferred : undefined;
200
203
  }
201
204
 
202
205
  /**
@@ -340,7 +343,7 @@ export function buildChildrenEntry(type: Function, property: string, metadataLis
340
343
  export function buildNestedEntry(type: Function, property: string): ChildrenDefinitionLike {
341
344
  const nestedType = resolveNestedType(type, property);
342
345
  const definition = createEmptyChildrenDefinition();
343
- definition.IdentifiedBy = '';
346
+ definition.IdentifiedBy = notSetPropertyPath;
344
347
 
345
348
  // A @clearWith on the property carrying @nested clears this nested object, the same as a
346
349
  // class-level @clearWith on the nested type itself (which populateFromType also honors).
@@ -3,12 +3,15 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the childrenFrom property decorator. */
9
10
  export interface ChildrenFromMetadata {
10
11
  /** The event constructor that adds children to the collection. */
11
12
  readonly eventType: Function;
13
+ /** Optional child type for TypeScript runtimes without emitted generic metadata. */
14
+ readonly childType?: Function;
12
15
  /** The event property name used as the key for children. Defaults to the event source identifier. */
13
16
  readonly key?: string;
14
17
  /** The child model property name used to uniquely identify instances in the collection. */
@@ -22,22 +25,25 @@ const METADATA_KEY = 'chronicle:projection:childrenFrom';
22
25
  /**
23
26
  * Property decorator that configures a children collection sub-projection from an event type.
24
27
  * @param eventType - The event constructor.
25
- * @param key - Optional event property name to use as the key for children.
28
+ * @param key - Optional event property name to use as the key for children, or the child constructor.
26
29
  * @param identifiedBy - Optional child model property name used to identify instances.
27
30
  * @param parentKey - Optional event property name used as the parent key.
28
31
  * @returns A property decorator.
29
32
  */
30
33
  export function childrenFrom(
31
34
  eventType: Function,
32
- key?: string,
35
+ key?: string | Function,
33
36
  identifiedBy?: string,
34
37
  parentKey?: string
35
38
  ): ChroniclePropertyDecorator {
36
- return decorateProperty((target: object, propertyKey: string | symbol) => {
39
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
37
40
  const propKey = propertyKey.toString();
38
41
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, propKey);
39
42
  const existing: ChildrenFromMetadata[] = Reflect.getMetadata(METADATA_KEY, target, propKey) ?? [];
40
- const metadata: ChildrenFromMetadata = { eventType, key, identifiedBy, parentKey };
43
+ const metadata: ChildrenFromMetadata = {
44
+ eventType, key: typeof key === 'string' ? key : undefined,
45
+ childType: typeof key === 'function' ? key : undefined, identifiedBy, parentKey
46
+ };
41
47
  Reflect.defineMetadata(METADATA_KEY, [...existing, metadata], target, propKey);
42
48
  });
43
49
  }
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChronicleClassOrPropertyDecorator, decorateClassOrProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundClassOrProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChronicleClassOrPropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the clearWith class or property decorator. */
9
10
  export interface ClearWithMetadata {
@@ -22,7 +23,7 @@ const PROPERTY_METADATA_KEY = 'chronicle:projection:clearWith:property';
22
23
  * @returns A class and property decorator.
23
24
  */
24
25
  export function clearWith(eventType: Function): ChronicleClassOrPropertyDecorator {
25
- return decorateClassOrProperty((target: object, propertyKey?: string | symbol) => {
26
+ return decorateModelBoundClassOrProperty((target: object, propertyKey?: string | symbol) => {
26
27
  if (propertyKey !== undefined) {
27
28
  const key = propertyKey.toString();
28
29
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the count property decorator. */
9
10
  export interface CountMetadata {
@@ -22,7 +23,7 @@ const METADATA_KEY = 'chronicle:projection:count';
22
23
  * @returns A property decorator.
23
24
  */
24
25
  export function count(eventType: Function, constantKey?: string): ChroniclePropertyDecorator {
25
- return decorateProperty((target: object, propertyKey: string | symbol) => {
26
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
26
27
  const key = propertyKey.toString();
27
28
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
28
29
  const existing: CountMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];