@cratis/chronicle 6.9.1 → 6.11.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 (288) 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 +270 -0
  8. package/connection/ChronicleConnection.ts +90 -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 +98 -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/eventSequences/AppendResult.d.ts +7 -0
  58. package/dist/eventSequences/AppendResult.d.ts.map +1 -1
  59. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  60. package/dist/eventSequences/EventSequence.js +26 -17
  61. package/dist/eventSequences/EventSequence.js.map +1 -1
  62. package/dist/eventSequences/WaitForCompletionOptions.d.ts +8 -0
  63. package/dist/eventSequences/WaitForCompletionOptions.d.ts.map +1 -0
  64. package/dist/eventSequences/WaitForCompletionOptions.js +4 -0
  65. package/dist/eventSequences/WaitForCompletionOptions.js.map +1 -0
  66. package/dist/eventSequences/index.d.ts +1 -0
  67. package/dist/eventSequences/index.d.ts.map +1 -1
  68. package/dist/eventSequences/index.js.map +1 -1
  69. package/dist/externalServices/ExternalServiceBuilder.d.ts +1 -1
  70. package/dist/externalServices/ExternalServiceBuilder.d.ts.map +1 -1
  71. package/dist/externalServices/ExternalServiceBuilder.js +1 -1
  72. package/dist/externalServices/ExternalServiceBuilder.js.map +1 -1
  73. package/dist/externalServices/ExternalServices.d.ts.map +1 -1
  74. package/dist/externalServices/ExternalServices.js +3 -1
  75. package/dist/externalServices/ExternalServices.js.map +1 -1
  76. package/dist/index.d.ts +1 -0
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +1 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/projections/Projections.d.ts.map +1 -1
  81. package/dist/projections/Projections.js +9 -7
  82. package/dist/projections/Projections.js.map +1 -1
  83. package/dist/projections/constantValueExpression.d.ts +3 -0
  84. package/dist/projections/constantValueExpression.d.ts.map +1 -0
  85. package/dist/projections/constantValueExpression.js +26 -0
  86. package/dist/projections/constantValueExpression.js.map +1 -0
  87. package/dist/projections/declarative/ChildrenBuilder.d.ts +2 -0
  88. package/dist/projections/declarative/ChildrenBuilder.d.ts.map +1 -1
  89. package/dist/projections/declarative/ChildrenBuilder.js +5 -0
  90. package/dist/projections/declarative/ChildrenBuilder.js.map +1 -1
  91. package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
  92. package/dist/projections/declarative/FromBuilder.js +3 -2
  93. package/dist/projections/declarative/FromBuilder.js.map +1 -1
  94. package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
  95. package/dist/projections/declarative/JoinBuilder.js +2 -1
  96. package/dist/projections/declarative/JoinBuilder.js.map +1 -1
  97. package/dist/projections/declarative/NestedBuilder.d.ts.map +1 -1
  98. package/dist/projections/declarative/NestedBuilder.js +3 -1
  99. package/dist/projections/declarative/NestedBuilder.js.map +1 -1
  100. package/dist/projections/declarative/ProjectionBuilderCore.d.ts +5 -0
  101. package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
  102. package/dist/projections/declarative/ProjectionBuilderCore.js +18 -2
  103. package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
  104. package/dist/projections/declarative/ProjectionBuilderFor.d.ts +6 -0
  105. package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
  106. package/dist/projections/declarative/ProjectionBuilderFor.js +5 -0
  107. package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
  108. package/dist/projections/declarative/SetBuilder.d.ts.map +1 -1
  109. package/dist/projections/declarative/SetBuilder.js +2 -1
  110. package/dist/projections/declarative/SetBuilder.js.map +1 -1
  111. package/dist/projections/modelBound/addFrom.d.ts.map +1 -1
  112. package/dist/projections/modelBound/addFrom.js +3 -2
  113. package/dist/projections/modelBound/addFrom.js.map +1 -1
  114. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts +20 -0
  115. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
  116. package/dist/projections/modelBound/childrenAndNestedBuilder.js +14 -11
  117. package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
  118. package/dist/projections/modelBound/childrenFrom.d.ts +4 -2
  119. package/dist/projections/modelBound/childrenFrom.d.ts.map +1 -1
  120. package/dist/projections/modelBound/childrenFrom.js +8 -4
  121. package/dist/projections/modelBound/childrenFrom.js.map +1 -1
  122. package/dist/projections/modelBound/clearWith.d.ts.map +1 -1
  123. package/dist/projections/modelBound/clearWith.js +3 -2
  124. package/dist/projections/modelBound/clearWith.js.map +1 -1
  125. package/dist/projections/modelBound/count.d.ts.map +1 -1
  126. package/dist/projections/modelBound/count.js +3 -2
  127. package/dist/projections/modelBound/count.js.map +1 -1
  128. package/dist/projections/modelBound/decrement.d.ts.map +1 -1
  129. package/dist/projections/modelBound/decrement.js +3 -2
  130. package/dist/projections/modelBound/decrement.js.map +1 -1
  131. package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
  132. package/dist/projections/modelBound/fromAll.js +3 -2
  133. package/dist/projections/modelBound/fromAll.js.map +1 -1
  134. package/dist/projections/modelBound/fromEvery.d.ts.map +1 -1
  135. package/dist/projections/modelBound/fromEvery.js +3 -2
  136. package/dist/projections/modelBound/fromEvery.js.map +1 -1
  137. package/dist/projections/modelBound/increment.d.ts.map +1 -1
  138. package/dist/projections/modelBound/increment.js +3 -2
  139. package/dist/projections/modelBound/increment.js.map +1 -1
  140. package/dist/projections/modelBound/join.d.ts.map +1 -1
  141. package/dist/projections/modelBound/join.js +3 -2
  142. package/dist/projections/modelBound/join.js.map +1 -1
  143. package/dist/projections/modelBound/nested.d.ts.map +1 -1
  144. package/dist/projections/modelBound/nested.js +3 -2
  145. package/dist/projections/modelBound/nested.js.map +1 -1
  146. package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -1
  147. package/dist/projections/modelBound/noAutoMap.js +3 -2
  148. package/dist/projections/modelBound/noAutoMap.js.map +1 -1
  149. package/dist/projections/modelBound/removedWith.d.ts.map +1 -1
  150. package/dist/projections/modelBound/removedWith.js +3 -2
  151. package/dist/projections/modelBound/removedWith.js.map +1 -1
  152. package/dist/projections/modelBound/removedWithJoin.d.ts.map +1 -1
  153. package/dist/projections/modelBound/removedWithJoin.js +3 -2
  154. package/dist/projections/modelBound/removedWithJoin.js.map +1 -1
  155. package/dist/projections/modelBound/setFrom.d.ts.map +1 -1
  156. package/dist/projections/modelBound/setFrom.js +3 -2
  157. package/dist/projections/modelBound/setFrom.js.map +1 -1
  158. package/dist/projections/modelBound/setFromContext.d.ts.map +1 -1
  159. package/dist/projections/modelBound/setFromContext.js +3 -2
  160. package/dist/projections/modelBound/setFromContext.js.map +1 -1
  161. package/dist/projections/modelBound/setValue.d.ts.map +1 -1
  162. package/dist/projections/modelBound/setValue.js +3 -2
  163. package/dist/projections/modelBound/setValue.js.map +1 -1
  164. package/dist/projections/modelBound/subtractFrom.d.ts.map +1 -1
  165. package/dist/projections/modelBound/subtractFrom.js +3 -2
  166. package/dist/projections/modelBound/subtractFrom.js.map +1 -1
  167. package/dist/projections/notSetPropertyPath.d.ts +3 -0
  168. package/dist/projections/notSetPropertyPath.d.ts.map +1 -0
  169. package/dist/projections/notSetPropertyPath.js +5 -0
  170. package/dist/projections/notSetPropertyPath.js.map +1 -0
  171. package/dist/reactors/Reactors.d.ts +4 -0
  172. package/dist/reactors/Reactors.d.ts.map +1 -1
  173. package/dist/reactors/Reactors.js +24 -6
  174. package/dist/reactors/Reactors.js.map +1 -1
  175. package/dist/reactors/reactor.d.ts +2 -2
  176. package/dist/reactors/reactor.js +2 -2
  177. package/dist/readModels/IReadModels.d.ts +3 -2
  178. package/dist/readModels/IReadModels.d.ts.map +1 -1
  179. package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
  180. package/dist/readModels/MaterializedReadModels.js +2 -4
  181. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  182. package/dist/readModels/ReadModels.d.ts +3 -5
  183. package/dist/readModels/ReadModels.d.ts.map +1 -1
  184. package/dist/readModels/ReadModels.js +17 -14
  185. package/dist/readModels/ReadModels.js.map +1 -1
  186. package/dist/readModels/deserializeReadModel.d.ts +5 -0
  187. package/dist/readModels/deserializeReadModel.d.ts.map +1 -0
  188. package/dist/readModels/deserializeReadModel.js +84 -0
  189. package/dist/readModels/deserializeReadModel.js.map +1 -0
  190. package/dist/readModels/rootReadModelTypes.d.ts +5 -0
  191. package/dist/readModels/rootReadModelTypes.d.ts.map +1 -0
  192. package/dist/readModels/rootReadModelTypes.js +69 -0
  193. package/dist/readModels/rootReadModelTypes.js.map +1 -0
  194. package/dist/reducers/Reducers.d.ts +4 -0
  195. package/dist/reducers/Reducers.d.ts.map +1 -1
  196. package/dist/reducers/Reducers.js +24 -6
  197. package/dist/reducers/Reducers.js.map +1 -1
  198. package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
  199. package/dist/schemas/JsonSchemaGenerator.js +4 -4
  200. package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
  201. package/dist/tsconfig.tsbuildinfo +1 -1
  202. package/dist/types/TypeDiscoverer.d.ts +8 -1
  203. package/dist/types/TypeDiscoverer.d.ts.map +1 -1
  204. package/dist/types/TypeDiscoverer.js +25 -5
  205. package/dist/types/TypeDiscoverer.js.map +1 -1
  206. package/dist/types/TypeIntrospector.d.ts.map +1 -1
  207. package/dist/types/TypeIntrospector.js +3 -2
  208. package/dist/types/TypeIntrospector.js.map +1 -1
  209. package/dist/types/conceptValueType.d.ts +4 -0
  210. package/dist/types/conceptValueType.d.ts.map +1 -0
  211. package/dist/types/conceptValueType.js +10 -0
  212. package/dist/types/conceptValueType.js.map +1 -0
  213. package/dist/types/modelBoundProperty.d.ts +6 -0
  214. package/dist/types/modelBoundProperty.d.ts.map +1 -0
  215. package/dist/types/modelBoundProperty.js +53 -0
  216. package/dist/types/modelBoundProperty.js.map +1 -0
  217. package/eventSequences/AppendResult.ts +8 -0
  218. package/eventSequences/EventSequence.spec.ts +76 -1
  219. package/eventSequences/EventSequence.ts +28 -20
  220. package/eventSequences/WaitForCompletionOptions.ts +11 -0
  221. package/eventSequences/index.ts +1 -0
  222. package/externalServices/ExternalServiceBuilder.ts +4 -4
  223. package/externalServices/ExternalServices.spec.ts +87 -0
  224. package/externalServices/ExternalServices.ts +3 -1
  225. package/for_ChronicleClient/connectionAuthentication.spec.ts +67 -0
  226. package/index.ts +1 -0
  227. package/observation/observationDisposal.spec.ts +74 -0
  228. package/package.json +2 -1
  229. package/projections/Projections.childrenAndNested.spec.ts +2 -0
  230. package/projections/Projections.modelBoundCompleteness.spec.ts +47 -0
  231. package/projections/Projections.ts +9 -7
  232. package/projections/constantValueExpression.spec.ts +31 -0
  233. package/projections/constantValueExpression.ts +28 -0
  234. package/projections/declarative/ChildrenBuilder.spec.ts +46 -1
  235. package/projections/declarative/ChildrenBuilder.ts +6 -0
  236. package/projections/declarative/FromBuilder.ts +3 -2
  237. package/projections/declarative/JoinBuilder.ts +2 -1
  238. package/projections/declarative/NestedBuilder.ts +3 -1
  239. package/projections/declarative/ProjectionBuilderCore.ts +20 -2
  240. package/projections/declarative/ProjectionBuilderFor.spec.ts +61 -4
  241. package/projections/declarative/ProjectionBuilderFor.ts +7 -0
  242. package/projections/declarative/SetBuilder.ts +2 -1
  243. package/projections/modelBound/addFrom.ts +3 -2
  244. package/projections/modelBound/childrenAndNestedBuilder.ts +14 -11
  245. package/projections/modelBound/childrenFrom.ts +11 -5
  246. package/projections/modelBound/clearWith.ts +3 -2
  247. package/projections/modelBound/count.ts +3 -2
  248. package/projections/modelBound/decorators.standard.spec.ts +45 -0
  249. package/projections/modelBound/decrement.ts +3 -2
  250. package/projections/modelBound/fromAll.ts +3 -2
  251. package/projections/modelBound/fromEvery.ts +3 -2
  252. package/projections/modelBound/increment.ts +3 -2
  253. package/projections/modelBound/join.ts +3 -2
  254. package/projections/modelBound/nested.ts +3 -2
  255. package/projections/modelBound/noAutoMap.ts +3 -2
  256. package/projections/modelBound/removedWith.ts +3 -2
  257. package/projections/modelBound/removedWithJoin.ts +3 -2
  258. package/projections/modelBound/setFrom.ts +3 -2
  259. package/projections/modelBound/setFromContext.ts +3 -2
  260. package/projections/modelBound/setValue.ts +3 -2
  261. package/projections/modelBound/subtractFrom.ts +3 -2
  262. package/projections/notSetPropertyPath.ts +5 -0
  263. package/reactors/Reactors.ts +22 -7
  264. package/reactors/reactor.ts +2 -2
  265. package/readModels/IReadModels.ts +3 -2
  266. package/readModels/MaterializedReadModels.spec.ts +22 -0
  267. package/readModels/MaterializedReadModels.ts +2 -4
  268. package/readModels/ReadModels.spec.ts +71 -6
  269. package/readModels/ReadModels.ts +18 -15
  270. package/readModels/conceptValueType.spec.ts +20 -0
  271. package/readModels/deserializeReadModel.spec.ts +84 -0
  272. package/readModels/deserializeReadModel.ts +82 -0
  273. package/readModels/discovery.spec.ts +27 -0
  274. package/readModels/rootReadModelTypes.spec.ts +115 -0
  275. package/readModels/rootReadModelTypes.ts +65 -0
  276. package/reducers/Reducers.ts +22 -7
  277. package/schemas/JsonSchemaGenerator.ts +4 -4
  278. package/schemas/legacyDesignMetadata.spec.ts +24 -2
  279. package/types/TypeDiscoverer.spec.ts +57 -0
  280. package/types/TypeDiscoverer.ts +27 -6
  281. package/types/TypeIntrospector.ts +3 -2
  282. package/types/conceptValueType.ts +11 -0
  283. package/types/modelBoundProperty.ts +53 -0
  284. package/dist/externalServices/ExternalServicesContracts.d.ts +0 -137
  285. package/dist/externalServices/ExternalServicesContracts.d.ts.map +0 -1
  286. package/dist/externalServices/ExternalServicesContracts.js +0 -654
  287. package/dist/externalServices/ExternalServicesContracts.js.map +0 -1
  288. package/externalServices/ExternalServicesContracts.ts +0 -709
@@ -2,7 +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 'reflect-metadata';
5
- import { field, type Constructor } from '@cratis/fundamentals';
5
+ import { ConceptAs, field, Guid, type Constructor } from '@cratis/fundamentals';
6
6
  import { describe, expect, it, vi } from 'vitest';
7
7
  import type { IClientArtifactsProvider } from '../artifacts/index.js';
8
8
  import type { ChronicleConnection } from '../connection/index.js';
@@ -10,6 +10,7 @@ import { subject } from '../compliance/subject.js';
10
10
  import { pii } from '../compliance/pii.js';
11
11
  import { reducer } from '../reducers/reducer.js';
12
12
  import { fromEvent } from '../projections/modelBound/fromEvent.js';
13
+ import { setFrom } from '../projections/modelBound/setFrom.js';
13
14
  import { ReadModels } from './ReadModels.js';
14
15
 
15
16
  // Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
@@ -49,7 +50,7 @@ describe('ReadModels', () => {
49
50
  class MissingModel { id = ''; }
50
51
  fromEvent(SomeEvent)(MissingModel);
51
52
 
52
- it('should retain the legacy prototype-only instance for getInstanceById', async () => {
53
+ it('should retain a prototype-only instance for getInstanceById when the document is empty', async () => {
53
54
  const { readModels, release } = createReadModels(MissingModel);
54
55
  const instance = await readModels.getInstanceById(MissingModel, 'missing');
55
56
  expect(instance).toBeInstanceOf(MissingModel);
@@ -65,13 +66,21 @@ describe('ReadModels', () => {
65
66
  expect(release).not.toHaveBeenCalled();
66
67
  });
67
68
 
68
- it('should return null when the kernel responds with literal JSON null', async () => {
69
+ it('should return null from findInstanceById when the kernel responds with JSON null', async () => {
69
70
  const { readModels, getInstanceByKey, release } = createReadModels(MissingModel);
70
71
  getInstanceByKey.mockResolvedValue({ ReadModel: 'null' });
71
72
  const instance = await readModels.findInstanceById(MissingModel, 'missing');
72
73
  expect(instance).toBeNull();
73
74
  expect(release).not.toHaveBeenCalled();
74
75
  });
76
+
77
+ it('should retain the constructed default instance from getInstanceById for JSON null', async () => {
78
+ const { readModels, getInstanceByKey } = createReadModels(MissingModel);
79
+ getInstanceByKey.mockResolvedValue({ ReadModel: 'null' });
80
+ const instance = await readModels.getInstanceById(MissingModel, 'missing');
81
+ expect(instance).toBeInstanceOf(MissingModel);
82
+ expect(instance.id).toBe('');
83
+ });
75
84
  });
76
85
 
77
86
  describe('when a read model instance exists for a key', () => {
@@ -85,9 +94,65 @@ describe('ReadModels', () => {
85
94
  const instance = await readModels.findInstanceById(ExistingModel, 'found');
86
95
  expect(instance).toBeInstanceOf(ExistingModel);
87
96
  expect(instance?.id).toBe('found');
88
- const legacyInstance = await readModels.getInstanceById(ExistingModel, 'found');
89
- expect(legacyInstance).toBeInstanceOf(ExistingModel);
90
- expect(legacyInstance.id).toBe('found');
97
+ const canonicalInstance = await readModels.getInstanceById(ExistingModel, 'found');
98
+ expect(canonicalInstance).toBeInstanceOf(ExistingModel);
99
+ expect(canonicalInstance.id).toBe('found');
100
+ });
101
+ });
102
+
103
+ describe('when reading a model with undecorated and model-bound members', () => {
104
+ class Code extends ConceptAs<string> {}
105
+ class MappedModel {
106
+ id = '';
107
+ name = '';
108
+ count = 0;
109
+ active = false;
110
+ occurred = new Date(0);
111
+ mapped = '';
112
+ code = new Code('initial');
113
+ guid = Guid.empty;
114
+ typed = Guid.empty;
115
+ }
116
+ field(Guid)(MappedModel.prototype, 'typed');
117
+ setFrom(SomeEvent, 'value')(MappedModel.prototype, 'mapped');
118
+ fromEvent(SomeEvent)(MappedModel);
119
+ const json = JSON.stringify({ id: 'a', name: 'stored', count: 42, active: true,
120
+ occurred: '2024-01-02T00:00:00.000Z', mapped: 'from event', code: 'stored-code',
121
+ guid: 'e951d9a0-720f-4e37-835f-701707f7b678',
122
+ typed: 'e951d9a0-720f-4e37-835f-701707f7b678', __internal: 'ignored' });
123
+
124
+ it('should restore all declared members for single and multiple reads', async () => {
125
+ const { readModels, getInstanceByKey } = createReadModels(MappedModel);
126
+ getInstanceByKey.mockResolvedValue({ ReadModel: json });
127
+ const getAllInstances = vi.fn().mockResolvedValue({ Instances: [json] });
128
+ const connection = { readModels: { getAllInstances } } as unknown as ChronicleConnection;
129
+ const provider = { readModels: [MappedModel], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
130
+ const all = await new ReadModels('store', 'tenant', connection, provider, 'sink').getInstances(MappedModel);
131
+ for (const instance of [await readModels.findInstanceById(MappedModel, 'a'),
132
+ await readModels.getInstanceById(MappedModel, 'a'), ...all]) {
133
+ expect(instance).toMatchObject({ id: 'a', name: 'stored', count: 42, active: true, mapped: 'from event' });
134
+ expect(instance?.occurred).toEqual(new Date('2024-01-02T00:00:00.000Z'));
135
+ expect(instance?.code).toBeInstanceOf(Code);
136
+ expect(instance?.code.value).toBe('stored-code');
137
+ expect(instance?.guid).toBeInstanceOf(Guid);
138
+ expect(instance?.typed).toBeInstanceOf(Guid);
139
+ expect(Object.hasOwn(instance as object, '__internal')).toBe(false);
140
+ }
141
+ });
142
+
143
+ it('should restore snapshot and watched instances', async () => {
144
+ const connection = { readModelExplorer: { allSnapshotsForReadModel: vi.fn().mockResolvedValue({ Data: [{ Instance: json }] }) },
145
+ readModels: { watch: async function* () {
146
+ yield { Namespace: 'tenant', ModelKey: 'a', ReadModel: json, Removed: false };
147
+ } } } as unknown as ChronicleConnection;
148
+ const provider = { readModels: [MappedModel], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
149
+ const readModels = new ReadModels('store', 'tenant', connection, provider, 'sink');
150
+ const snapshots = await readModels.getSnapshotsById(MappedModel, 'a');
151
+ const watched = await readModels.watch(MappedModel)[Symbol.asyncIterator]().next();
152
+ expect(snapshots[0].readModel.mapped).toBe('from event');
153
+ expect(snapshots[0].readModel.occurred).toBeInstanceOf(Date);
154
+ expect(watched.value?.readModel.name).toBe('stored');
155
+ expect(watched.value?.readModel.occurred).toBeInstanceOf(Date);
91
156
  });
92
157
  });
93
158
 
@@ -24,9 +24,11 @@ import { JsonSchemaGenerator } from '../schemas/index.js';
24
24
  import { WellKnownSinks } from '../sinks/index.js';
25
25
  import { getReadModelMetadata, getReadModelId } from './readModel.js';
26
26
  import { assertUniqueReadModelIds } from './assertUniqueReadModelIds.js';
27
+ import { rootReadModelTypes } from './rootReadModelTypes.js';
27
28
  import type { IMaterializedReadModels } from './IMaterializedReadModels.js';
28
29
  import { MaterializedReadModels } from './MaterializedReadModels.js';
29
30
  import { ReadModelSubjectResolver } from './ReadModelSubjectResolver.js';
31
+ import { deserializeReadModel } from './deserializeReadModel.js';
30
32
  import type { IReadModels } from './IReadModels.js';
31
33
  import type { ReadModelChangeset } from './ReadModelChangeset.js';
32
34
  import type { ReadModelSnapshot } from './ReadModelSnapshot.js';
@@ -41,6 +43,7 @@ interface ResolvedReadModel {
41
43
  readonly observerIdentifier: string;
42
44
  readonly schema: string;
43
45
  readonly isActive: boolean;
46
+ readonly isModelBound?: boolean;
44
47
  }
45
48
 
46
49
  /**
@@ -54,7 +57,8 @@ export class ReadModels implements IReadModels {
54
57
  private readonly _namespace: string,
55
58
  private readonly _connection: ChronicleConnection,
56
59
  private readonly _clientArtifacts: IClientArtifactsProvider,
57
- private readonly _defaultSinkTypeId: string
60
+ private readonly _defaultSinkTypeId: string,
61
+ private readonly _isModelBoundProjectionRegistered?: (readModelType: Constructor) => boolean
58
62
  ) {
59
63
  this.materialized = new MaterializedReadModels(_eventStore, _namespace, _connection);
60
64
  }
@@ -74,10 +78,7 @@ export class ReadModels implements IReadModels {
74
78
  });
75
79
  }
76
80
 
77
- /**
78
- * @inheritdoc
79
- * @deprecated Use {@link findInstanceById} to distinguish an absent instance from a stored one.
80
- */
81
+ /** @inheritdoc */
81
82
  async getInstanceById<TReadModel>(readModelType: Constructor<TReadModel>, key: string, sessionId?: string): Promise<TReadModel> {
82
83
  const readModel = this.resolveReadModel(readModelType);
83
84
  const response = await this._connection.readModels.getInstanceByKey({
@@ -245,7 +246,8 @@ export class ReadModels implements IReadModels {
245
246
  }
246
247
 
247
248
  private resolveReadModels<TReadModel>(readModelType?: Constructor<TReadModel>): ResolvedReadModel[] {
248
- assertUniqueReadModelIds(this._clientArtifacts.readModels);
249
+ const rootTypes = rootReadModelTypes(this._clientArtifacts);
250
+ assertUniqueReadModelIds(rootTypes);
249
251
  const resolved = new Map<string, ResolvedReadModel>();
250
252
 
251
253
  for (const projectionType of this._clientArtifacts.projections) {
@@ -270,7 +272,7 @@ export class ReadModels implements IReadModels {
270
272
  });
271
273
  }
272
274
 
273
- for (const modelBoundType of this._clientArtifacts.readModels) {
275
+ for (const modelBoundType of rootTypes) {
274
276
  if (!hasFromEventMetadata(modelBoundType) && !hasModelBoundProperties(modelBoundType)) {
275
277
  continue;
276
278
  }
@@ -287,7 +289,8 @@ export class ReadModels implements IReadModels {
287
289
  observerType: ContractReadModelObserverType.Projection,
288
290
  observerIdentifier: identifier,
289
291
  schema: this.getReadModelSchema(modelBoundType, identifier),
290
- isActive: !isPassive(modelBoundType)
292
+ isActive: !isPassive(modelBoundType),
293
+ isModelBound: true
291
294
  });
292
295
  }
293
296
 
@@ -327,13 +330,16 @@ export class ReadModels implements IReadModels {
327
330
 
328
331
  private resolveReadModel<TReadModel>(readModelType: Constructor<TReadModel>): ResolvedReadModel {
329
332
  const [resolved] = this.resolveReadModels(readModelType);
330
- if (!resolved) {
331
- throw new Error(`Unknown read model '${readModelType.name}'. Make sure it is discoverable through a projection, reducer, or model-bound mapping.`);
333
+ const neverRegistered = resolved?.isModelBound && this._isModelBoundProjectionRegistered && !this._isModelBoundProjectionRegistered(readModelType);
334
+ if (!resolved || neverRegistered) {
335
+ throw new Error(hasModelBoundProperties(readModelType)
336
+ ? `Unknown read model '${readModelType.name}'. It has model-bound property mappings but was not registered when the event store was created. With standard decorators, a class whose mappings are all on properties is only registered once an instance exists; add a class-level @fromEvent(...) decorator so it registers when its module loads.`
337
+ : `Unknown read model '${readModelType.name}'. Make sure it is discoverable through a projection, reducer, or model-bound mapping.`);
332
338
  }
333
-
334
339
  return resolved;
335
340
  }
336
341
 
342
+
337
343
  private toDefinition(readModel: ResolvedReadModel) {
338
344
  return {
339
345
  Type: {
@@ -367,10 +373,7 @@ export class ReadModels implements IReadModels {
367
373
  }
368
374
 
369
375
  private deserializeReadModel<TReadModel>(readModelType: Constructor<TReadModel>, json: string): TReadModel {
370
- if (!json) {
371
- return Object.create(readModelType.prototype) as TReadModel;
372
- }
373
- return JsonSerializer.deserialize(readModelType as Constructor<object>, json) as TReadModel;
376
+ return deserializeReadModel(readModelType, json);
374
377
  }
375
378
 
376
379
  private schemaHasComplianceMetadata(schema: string): boolean {
@@ -0,0 +1,20 @@
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
+ import { ConceptAs, Guid } from '@cratis/fundamentals';
5
+ import { describe, expect, it } from 'vitest';
6
+ import { deserializeReadModel } from './deserializeReadModel.js';
7
+
8
+ class Cid extends ConceptAs<Guid> { static readonly valueType = Guid; }
9
+ class BirthDate extends ConceptAs<Date> { static readonly valueType = Date; }
10
+ class Concepts { cid = new Cid(Guid.empty); birth = new BirthDate(new Date(0)); }
11
+
12
+ describe('concept value restoration', () => {
13
+ it('uses static valueType for Guid and Date without design metadata', () => {
14
+ const concepts = deserializeReadModel(Concepts, JSON.stringify({ cid: 'f417bba6-5737-488a-a225-37da46b96221', birth: '2025-01-02T00:00:00.000Z' }));
15
+ expect(concepts.cid).toBeInstanceOf(Cid);
16
+ expect(concepts.cid.value).toBeInstanceOf(Guid);
17
+ expect(concepts.birth).toBeInstanceOf(BirthDate);
18
+ expect(concepts.birth.value).toBeInstanceOf(Date);
19
+ });
20
+ });
@@ -0,0 +1,84 @@
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
+ import 'reflect-metadata';
5
+ import { ConceptAs, Guid, field } from '@cratis/fundamentals';
6
+ import { describe, expect, it } from 'vitest';
7
+ import { TypeIntrospector } from '../types/TypeIntrospector.js';
8
+ import { deserializeReadModel } from './deserializeReadModel.js';
9
+
10
+ class Identity extends ConceptAs<Guid> {}
11
+ Reflect.defineMetadata('design:paramtypes', [Guid], Identity);
12
+ class Model {
13
+ date!: Date;
14
+ id!: Guid;
15
+ identity!: Identity;
16
+ enabled!: boolean;
17
+ get computed(): string { return 'computed'; }
18
+ }
19
+ for (const name of ['date', 'id', 'identity', 'enabled', 'computed']) TypeIntrospector.trackProperty(Model, name);
20
+ Reflect.defineMetadata('design:type', Date, Model.prototype, 'date');
21
+ Reflect.defineMetadata('design:type', Guid, Model.prototype, 'id');
22
+ Reflect.defineMetadata('design:type', Identity, Model.prototype, 'identity');
23
+ Reflect.defineMetadata('design:type', Boolean, Model.prototype, 'enabled');
24
+
25
+ class Inferred {
26
+ date = new Date(0);
27
+ id = Guid.empty;
28
+ }
29
+
30
+ class Address { street = ''; city = ''; }
31
+ class DecoratedAddress { street = ''; city = ''; }
32
+ field(String)(DecoratedAddress.prototype, 'street');
33
+ class Person { address = new Address(); decorated = new DecoratedAddress(); children: Address[] = []; }
34
+ field(Array, { genericArguments: [Address] })(Person.prototype, 'children');
35
+
36
+ const guid = 'f417bba6-5737-488a-a225-37da46b96221';
37
+
38
+ describe('deserializeReadModel', () => {
39
+ it('restores declared values, concepts and false strings while skipping read-only accessors', () => {
40
+ const model = deserializeReadModel(Model, JSON.stringify({
41
+ date: '2025-01-02T00:00:00.000Z', id: guid, identity: guid, enabled: 'false', computed: 'stored'
42
+ }));
43
+ expect(model.date).toBeInstanceOf(Date);
44
+ expect(model.id).toBeInstanceOf(Guid);
45
+ expect(model.identity.value).toBeInstanceOf(Guid);
46
+ expect(model.enabled).toBe(false);
47
+ expect(model.computed).toBe('computed');
48
+ });
49
+
50
+ it('uses inferred member types without legacy design metadata', () => {
51
+ const model = deserializeReadModel(Inferred, JSON.stringify({ date: '2025-01-02T00:00:00.000Z', id: guid }));
52
+ expect(model.date).toBeInstanceOf(Date);
53
+ expect(model.id).toBeInstanceOf(Guid);
54
+ });
55
+
56
+ it('restores nested plain members, mixed decorated members and typed child arrays', () => {
57
+ const person = deserializeReadModel(Person, JSON.stringify({
58
+ address: { street: 'Main', city: 'Oslo' },
59
+ decorated: { street: 'Oak', city: 'Rome' },
60
+ children: [{ street: 'First', city: 'Paris' }, { street: 'Second', city: 'Berlin' }]
61
+ }));
62
+ expect(person.address).toBeInstanceOf(Address);
63
+ expect(person.address).toEqual({ street: 'Main', city: 'Oslo' });
64
+ expect(person.decorated).toBeInstanceOf(DecoratedAddress);
65
+ expect(person.decorated).toEqual({ street: 'Oak', city: 'Rome' });
66
+ expect(person.children).toHaveLength(2);
67
+ expect(person.children[0]).toBeInstanceOf(Address);
68
+ expect(person.children[1]).toEqual({ street: 'Second', city: 'Berlin' });
69
+ });
70
+
71
+ it('introspects members only once for multiple rows', () => {
72
+ const getMembers = TypeIntrospector.getMembers;
73
+ let calls = 0;
74
+ TypeIntrospector.getMembers = function (...args) { calls++; return getMembers.apply(this, args); };
75
+ try {
76
+ class Rows { enabled = false; }
77
+ deserializeReadModel(Rows, '{"enabled":false}');
78
+ deserializeReadModel(Rows, '{"enabled":false}');
79
+ expect(calls).toBe(1);
80
+ } finally {
81
+ TypeIntrospector.getMembers = getMembers;
82
+ }
83
+ });
84
+ });
@@ -0,0 +1,82 @@
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
+ import 'reflect-metadata';
5
+ import { conceptAsTypeKey, type Constructor, Fields, Guid, JsonSerializer, typeKeyOf } from '@cratis/fundamentals';
6
+ import { TypeIntrospector } from '../types/TypeIntrospector.js';
7
+ import { conceptValueType } from '../types/conceptValueType.js';
8
+ import { getChildrenFromMetadata } from '../projections/modelBound/childrenFrom.js';
9
+
10
+ const memberCache = new WeakMap<Function, Map<string, Function | undefined>>();
11
+
12
+ function membersFor(type: Function): Map<string, Function | undefined> {
13
+ let members = memberCache.get(type);
14
+ if (!members) {
15
+ members = TypeIntrospector.getMembers(type);
16
+ memberCache.set(type, members);
17
+ }
18
+ return members;
19
+ }
20
+
21
+ function writable(type: Function, name: string): boolean {
22
+ let prototype = type.prototype;
23
+ while (prototype) {
24
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
25
+ if (descriptor) return !descriptor.get || !!descriptor.set;
26
+ prototype = Object.getPrototypeOf(prototype);
27
+ }
28
+ return true;
29
+ }
30
+
31
+ function convert(type: Function, value: unknown): unknown {
32
+ if (value === null || value === undefined) return value;
33
+ if (typeKeyOf(type as Constructor) === conceptAsTypeKey) {
34
+ const underlying = conceptValueType(type);
35
+ return Reflect.construct(type, [underlying ? convert(underlying, value) : value]);
36
+ }
37
+ if (type === Boolean) return typeof value === 'string' && /^(true|false)$/i.test(value) ? value.toLowerCase() === 'true' : value;
38
+ if (type === Number) return Number(value);
39
+ if (type === String) return String(value);
40
+ if (type === Date || type === Guid || typeKeyOf(type as Constructor) === 'Guid') {
41
+ return JsonSerializer.deserializeFromInstance(type as Constructor<object>, value);
42
+ }
43
+ if (typeof value === 'object' && type !== Array && type !== Object && !Array.isArray(value)) {
44
+ return restore(type as Constructor<object>, value as Record<string, unknown>);
45
+ }
46
+ return value;
47
+ }
48
+
49
+ function restore<T>(type: Constructor<T>, stored: Record<string, unknown> | null): T {
50
+ const fields = Fields.getFieldsForType(type);
51
+ const instance = fields.length > 0
52
+ ? JsonSerializer.deserializeFromInstance(type as Constructor<object>, stored) as T
53
+ : Reflect.construct(type, []) as T;
54
+ if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return instance;
55
+
56
+ const fieldByName = new Map(fields.map(field => [field.name, field]));
57
+ const result = instance as Record<string, unknown>;
58
+ for (const [name, runtimeType] of membersFor(type)) {
59
+ if (name.startsWith('__') || !writable(type, name) || !Object.prototype.hasOwnProperty.call(stored, name)) continue;
60
+ const value = stored[name];
61
+ const field = fieldByName.get(name);
62
+ const childType = getChildrenFromMetadata(type.prototype, name).find(metadata => metadata.childType)?.childType ?? field?.genericArguments?.[0];
63
+ if (Array.isArray(value) && childType) {
64
+ result[name] = value.map(item => convert(childType, item));
65
+ } else if (field && (value === null || value === undefined || !runtimeType || runtimeType === Array || runtimeType === Object || typeof value !== 'object')) {
66
+ // The serializer has already handled decorated scalar fields.
67
+ if (value === null || value === undefined) result[name] = value;
68
+ } else {
69
+ result[name] = value === null || value === undefined || !runtimeType ? value : convert(runtimeType, value);
70
+ }
71
+ }
72
+ return instance;
73
+ }
74
+
75
+ /** Restores declared model members not covered by Fundamentals' @field-based deserializer. */
76
+ export function deserializeReadModel<TReadModel>(readModelType: Constructor<TReadModel>, json: string): TReadModel {
77
+ if (!json) {
78
+ return Object.create(readModelType.prototype) as TReadModel;
79
+ }
80
+
81
+ return restore(readModelType, JSON.parse(json) as Record<string, unknown> | null);
82
+ }
@@ -67,6 +67,33 @@ describe('when discovering read models from observers', () => {
67
67
  expect(JSON.parse(definition.Schema).properties.value.type).toBe('string');
68
68
  expect(register.mock.calls.some(call => call[0].Projections[0].ReadModel === 'MappedOnly')).toBe(true);
69
69
  });
70
+ it('should register a property-bound model without file discovery', async () => {
71
+ class DecoratedOnly { value = ''; }
72
+ setFrom(Event)(DecoratedOnly.prototype, 'value');
73
+ setFrom(Event)(DecoratedOnly.prototype, 'value');
74
+ expect(DefaultClientArtifactsProvider.default.readModels.filter(type => type === DecoratedOnly)).toHaveLength(1);
75
+ const { connection, registerMany } = createConnection();
76
+ await new ReadModels('store', 'Default', connection, DefaultClientArtifactsProvider.default, 'sink').register(DecoratedOnly);
77
+ expect(registerMany.mock.calls[0][0].ReadModels[0].Type.Identifier).toBe('DecoratedOnly');
78
+ });
79
+
80
+ it('should refuse to query a model-bound read model whose projection was never registered', async () => {
81
+ class RegisteredLate { value = ''; }
82
+ setFrom(Event)(RegisteredLate.prototype, 'value');
83
+ const { connection } = createConnection();
84
+ const readModels = new ReadModels('store', 'Default', connection, DefaultClientArtifactsProvider.default, 'sink', () => false);
85
+ await expect(readModels.findInstanceById(RegisteredLate, 'key')).rejects.toThrow(/add a class-level @fromEvent/);
86
+ });
87
+
88
+ it('should explain how to register an unregistered property-bound model', async () => {
89
+ class NeverRegistered { value = ''; }
90
+ Reflect.defineMetadata('chronicle:projection:setFrom', [{ eventType: Event }], NeverRegistered.prototype, 'value');
91
+ Reflect.defineMetadata('chronicle:typeIntrospection:properties', ['value'], NeverRegistered);
92
+ const { connection } = createConnection();
93
+ await expect(new ReadModels('store', 'Default', connection, DefaultClientArtifactsProvider.default, 'sink').findInstanceById(NeverRegistered, 'key'))
94
+ .rejects.toThrow(/add a class-level @fromEvent/);
95
+ });
96
+
70
97
  it('should deduplicate inferred types and retain custom identifiers', async () => {
71
98
  const provider = DefaultClientArtifactsProvider.default;
72
99
  for (const type of [ModelBound, Projected, Reduced]) {
@@ -0,0 +1,115 @@
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
+ import 'reflect-metadata';
5
+ import { field } from '@cratis/fundamentals';
6
+ import { describe, expect, it } from 'vitest';
7
+ import type { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
8
+ import { eventType } from '../events/eventTypeDecorator.js';
9
+ import { childrenFrom } from '../projections/modelBound/childrenFrom.js';
10
+ import { nested } from '../projections/modelBound/nested.js';
11
+ import { TypeDiscoverer } from '../types/TypeDiscoverer.js';
12
+ import { DecoratorType } from '../types/DecoratorType.js';
13
+ import { DefaultClientArtifactsProvider } from '../artifacts/DefaultClientArtifactsProvider.js';
14
+ import { JsonSchemaGenerator } from '../schemas/JsonSchemaGenerator.js';
15
+ import { projection } from '../projections/declarative/projection.js';
16
+ import type { IProjectionBuilderFor } from '../projections/declarative/IProjectionBuilderFor.js';
17
+ import { setFrom } from '../projections/modelBound/setFrom.js';
18
+ import { rootReadModelTypes } from './rootReadModelTypes.js';
19
+ import { Projections } from '../projections/Projections.js';
20
+ import type { ChronicleConnection } from '../connection/ChronicleConnection.js';
21
+ import { assertUniqueReadModelIds } from './assertUniqueReadModelIds.js';
22
+
23
+ class Added { value!: string; }
24
+ eventType()(Added);
25
+
26
+ function parentWithChild() {
27
+ class Item { value!: string; }
28
+ setFrom(Added)(Item.prototype, 'value');
29
+ class Parent { items!: Item[]; }
30
+ field(Array, { genericArguments: [Item] })(Parent.prototype, 'items');
31
+ childrenFrom(Added)(Parent.prototype, 'items');
32
+ return { Parent, Item };
33
+ }
34
+
35
+ describe('root model-bound discovery', () => {
36
+ it('discovers only the parent as a root projection', async () => {
37
+ const { Parent, Item } = parentWithChild();
38
+ const artifacts = { readModels: [Parent, Item], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
39
+ const projections = new Projections('test', 'Default', {} as ChronicleConnection, artifacts, 'sink');
40
+ await projections.discover();
41
+ expect(projections.hasFor('Parent')).toBe(true);
42
+ expect(projections.hasFor('Item')).toBe(false);
43
+ });
44
+
45
+ it('does not register a child with its own setFrom as a root', () => {
46
+ const { Parent, Item } = parentWithChild();
47
+ const artifacts = { readModels: [Parent, Item], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
48
+ expect(rootReadModelTypes(artifacts)).toEqual([Parent]);
49
+ });
50
+
51
+ it('resolves children from decorator metadata without design metadata', () => {
52
+ class Item { value = ''; }
53
+ setFrom(Added)(Item.prototype, 'value');
54
+ class Parent { items: Item[] = []; }
55
+ childrenFrom(Added, Item)(Parent.prototype, 'items');
56
+ const artifacts = { readModels: [Parent, Item], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
57
+ expect(rootReadModelTypes(artifacts)).toEqual([Parent]);
58
+ expect(JsonSchemaGenerator.generate(Parent, undefined, true).properties?.items.items?.properties?.value.type).toBe('string');
59
+ const projections = new Projections('test', 'Default', {} as ChronicleConnection, artifacts, 'sink');
60
+ return projections.discover().then(() => {
61
+ expect(projections.hasFor('Parent')).toBe(true);
62
+ expect(projections.hasFor('Item')).toBe(false);
63
+ });
64
+ });
65
+
66
+ it('excludes nested default-instance members and their grandchildren', () => {
67
+ class Leaf { value = ''; }
68
+ setFrom(Added)(Leaf.prototype, 'value');
69
+ class Address { leaf = new Leaf(); }
70
+ nested(Address.prototype, 'leaf');
71
+ class Parent { addr = new Address(); }
72
+ nested(Parent.prototype, 'addr');
73
+ const artifacts = { readModels: [Parent, Address, Leaf], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
74
+ expect(rootReadModelTypes(artifacts)).toEqual([Parent]);
75
+ });
76
+
77
+ it('excludes a typed child referenced only by a declarative children builder', () => {
78
+ class Child { value = ''; }
79
+ setFrom(Added)(Child.prototype, 'value');
80
+ class Parent { children: Child[] = []; }
81
+ field(Array, { genericArguments: [Child] })(Parent.prototype, 'children');
82
+ class ParentProjection {
83
+ define(builder: IProjectionBuilderFor<Parent>) { builder.children<Child>(parent => parent.children, () => {}); }
84
+ }
85
+ projection('parent-projection', Parent)(ParentProjection);
86
+ const artifacts = { readModels: [Parent, Child], projections: [ParentProjection], reducers: [] } as unknown as IClientArtifactsProvider;
87
+ expect(rootReadModelTypes(artifacts)).toEqual([Parent]);
88
+ });
89
+
90
+ it('retains a self-referencing tree as a root', () => {
91
+ class Node { children: Node[] = []; }
92
+ childrenFrom(Added, Node)(Node.prototype, 'children');
93
+ const artifacts = { readModels: [Node], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
94
+ expect(rootReadModelTypes(artifacts)).toEqual([Node]);
95
+ });
96
+
97
+ it('does not eagerly register a property-only child as a root', () => {
98
+ const discoverer = new TypeDiscoverer();
99
+ const before = discoverer.getTypesByDecoratorType(DecoratorType.ReadModel);
100
+ const { Parent, Item } = parentWithChild();
101
+ expect(discoverer.getTypesByDecoratorType(DecoratorType.ReadModel)).toEqual(before);
102
+ const artifacts = new DefaultClientArtifactsProvider(discoverer);
103
+ expect(rootReadModelTypes(artifacts)).toContain(Parent);
104
+ expect(rootReadModelTypes(artifacts)).not.toContain(Item);
105
+ });
106
+
107
+ it('permits equal-named child types under different parents', () => {
108
+ const first = parentWithChild();
109
+ const second = parentWithChild();
110
+ Object.defineProperty(second.Parent, 'name', { value: 'OtherParent' });
111
+ const artifacts = { readModels: [first.Parent, first.Item, second.Item, second.Parent], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
112
+ expect(() => assertUniqueReadModelIds(rootReadModelTypes(artifacts))).not.toThrow();
113
+ expect(rootReadModelTypes(artifacts)).toEqual([first.Parent, second.Parent]);
114
+ });
115
+ });
@@ -0,0 +1,65 @@
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
+ import { type Constructor } from '@cratis/fundamentals';
5
+ import type { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
6
+ import { getProjectionMetadata } from '../projections/declarative/projection.js';
7
+ import { ProjectionBuilderFor } from '../projections/declarative/ProjectionBuilderFor.js';
8
+ import type { IProjectionFor } from '../projections/declarative/IProjectionFor.js';
9
+ import type { ChildrenDefinitionLike } from '../projections/modelBound/childrenAndNestedBuilder.js';
10
+ import { getChildrenFromMetadata } from '../projections/modelBound/childrenFrom.js';
11
+ import { resolveChildElementType, resolveNestedType } from '../projections/modelBound/childrenAndNestedBuilder.js';
12
+ import { isNested } from '../projections/modelBound/nested.js';
13
+ import { getReducerMetadata } from '../reducers/reducer.js';
14
+ import { TypeIntrospector } from '../types/TypeIntrospector.js';
15
+
16
+ /** Excludes models used as children or nested objects from root projection registration. */
17
+ export function rootReadModelTypes(artifacts: IClientArtifactsProvider): Constructor[] {
18
+ const models = artifacts.readModels;
19
+ const usedAsChildren = new Set<Function>();
20
+ const visited = new Set<Function>();
21
+ const inspect = (type: Function): void => {
22
+ if (visited.has(type)) return;
23
+ visited.add(type);
24
+ for (const property of TypeIntrospector.getTrackedProperties(type)) {
25
+ const child = getChildrenFromMetadata(type.prototype, property).length > 0;
26
+ const nested = isNested(type.prototype, property);
27
+ if (!child && !nested) continue;
28
+ const referenced = child ? resolveChildElementType(type, property) : resolveNestedType(type, property);
29
+ if (referenced && referenced !== type && referenced !== Array && referenced !== Object) {
30
+ usedAsChildren.add(referenced);
31
+ inspect(referenced);
32
+ }
33
+ }
34
+ };
35
+ for (const type of models) inspect(type);
36
+ const inspectDeclarative = (type: Function, definitions: { children: Record<string, ChildrenDefinitionLike>; nested: Record<string, ChildrenDefinitionLike> }): void => {
37
+ for (const [property, definition] of Object.entries(definitions.children)) {
38
+ const referenced = resolveChildElementType(type, property);
39
+ if (!referenced) continue;
40
+ if (referenced !== type) usedAsChildren.add(referenced);
41
+ inspect(referenced);
42
+ inspectDeclarative(referenced, { children: definition.Children, nested: definition.Nested });
43
+ }
44
+ for (const [property, definition] of Object.entries(definitions.nested)) {
45
+ const referenced = resolveNestedType(type, property);
46
+ if (!referenced) continue;
47
+ if (referenced !== type) usedAsChildren.add(referenced);
48
+ inspect(referenced);
49
+ inspectDeclarative(referenced, { children: definition.Children, nested: definition.Nested });
50
+ }
51
+ };
52
+ for (const type of artifacts.projections) {
53
+ const model = getProjectionMetadata(type)?.readModelType;
54
+ if (!model) continue;
55
+ inspect(model);
56
+ const builder = new ProjectionBuilderFor<unknown>();
57
+ (new type() as IProjectionFor<unknown>).define(builder);
58
+ inspectDeclarative(model, builder.getSubobjectDefinitions());
59
+ }
60
+ for (const type of artifacts.reducers) {
61
+ const model = getReducerMetadata(type)?.readModel;
62
+ if (model) inspect(model);
63
+ }
64
+ return models.filter(type => !usedAsChildren.has(type));
65
+ }