@cratis/chronicle 3.1.0 → 4.0.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 (217) hide show
  1. package/ChronicleClient.ts +4 -3
  2. package/EventStore.ts +4 -3
  3. package/README.md +40 -28
  4. package/compliance/ComplianceContracts.ts +59 -0
  5. package/compliance/IPIIManager.ts +12 -0
  6. package/compliance/PIIManager.spec.ts +50 -0
  7. package/compliance/PIIManager.ts +9 -0
  8. package/compliance/PIINotSupportedOnEventSourceId.ts +21 -0
  9. package/compliance/index.ts +2 -0
  10. package/compliance/pii.spec.ts +37 -0
  11. package/compliance/pii.ts +16 -1
  12. package/compliance/subject.spec.ts +45 -0
  13. package/compliance/subject.ts +64 -0
  14. package/connection/callResults.spec.ts +90 -0
  15. package/connection/callResults.ts +112 -0
  16. package/dist/ChronicleClient.d.ts.map +1 -1
  17. package/dist/ChronicleClient.js +4 -3
  18. package/dist/ChronicleClient.js.map +1 -1
  19. package/dist/EventStore.d.ts.map +1 -1
  20. package/dist/EventStore.js +4 -3
  21. package/dist/EventStore.js.map +1 -1
  22. package/dist/compliance/ComplianceContracts.d.ts +31 -0
  23. package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
  24. package/dist/compliance/ComplianceContracts.js +52 -0
  25. package/dist/compliance/ComplianceContracts.js.map +1 -1
  26. package/dist/compliance/IPIIManager.d.ts +11 -0
  27. package/dist/compliance/IPIIManager.d.ts.map +1 -1
  28. package/dist/compliance/PIIManager.d.ts +2 -0
  29. package/dist/compliance/PIIManager.d.ts.map +1 -1
  30. package/dist/compliance/PIIManager.js +8 -0
  31. package/dist/compliance/PIIManager.js.map +1 -1
  32. package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts +11 -0
  33. package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts.map +1 -0
  34. package/dist/compliance/PIINotSupportedOnEventSourceId.js +19 -0
  35. package/dist/compliance/PIINotSupportedOnEventSourceId.js.map +1 -0
  36. package/dist/compliance/index.d.ts +2 -0
  37. package/dist/compliance/index.d.ts.map +1 -1
  38. package/dist/compliance/index.js +2 -0
  39. package/dist/compliance/index.js.map +1 -1
  40. package/dist/compliance/pii.d.ts.map +1 -1
  41. package/dist/compliance/pii.js +13 -1
  42. package/dist/compliance/pii.js.map +1 -1
  43. package/dist/compliance/subject.d.ts +39 -0
  44. package/dist/compliance/subject.d.ts.map +1 -0
  45. package/dist/compliance/subject.js +58 -0
  46. package/dist/compliance/subject.js.map +1 -0
  47. package/dist/connection/callResults.d.ts +69 -0
  48. package/dist/connection/callResults.d.ts.map +1 -0
  49. package/dist/connection/callResults.js +74 -0
  50. package/dist/connection/callResults.js.map +1 -0
  51. package/dist/eventSequences/AppendOptions.d.ts +8 -0
  52. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  53. package/dist/eventSequences/EventForEventSourceId.d.ts +7 -0
  54. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  55. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  56. package/dist/eventSequences/EventSequence.js +17 -6
  57. package/dist/eventSequences/EventSequence.js.map +1 -1
  58. package/dist/events/EventContext.d.ts +3 -0
  59. package/dist/events/EventContext.d.ts.map +1 -1
  60. package/dist/events/Tag.d.ts +10 -0
  61. package/dist/events/Tag.d.ts.map +1 -0
  62. package/dist/events/Tag.js +16 -0
  63. package/dist/events/Tag.js.map +1 -0
  64. package/dist/events/constraints/Constraints.js +3 -3
  65. package/dist/events/constraints/Constraints.js.map +1 -1
  66. package/dist/events/filterEventsByTagDecorator.d.ts +35 -0
  67. package/dist/events/filterEventsByTagDecorator.d.ts.map +1 -0
  68. package/dist/events/filterEventsByTagDecorator.js +48 -0
  69. package/dist/events/filterEventsByTagDecorator.js.map +1 -0
  70. package/dist/events/index.d.ts +4 -0
  71. package/dist/events/index.d.ts.map +1 -1
  72. package/dist/events/index.js +4 -0
  73. package/dist/events/index.js.map +1 -1
  74. package/dist/events/mergeTags.d.ts +12 -0
  75. package/dist/events/mergeTags.d.ts.map +1 -0
  76. package/dist/events/mergeTags.js +26 -0
  77. package/dist/events/mergeTags.js.map +1 -0
  78. package/dist/events/tagDecorator.d.ts +38 -0
  79. package/dist/events/tagDecorator.d.ts.map +1 -0
  80. package/dist/events/tagDecorator.js +54 -0
  81. package/dist/events/tagDecorator.js.map +1 -0
  82. package/dist/jobs/IJobs.d.ts +4 -4
  83. package/dist/jobs/IJobs.d.ts.map +1 -1
  84. package/dist/jobs/Jobs.d.ts +4 -4
  85. package/dist/jobs/Jobs.d.ts.map +1 -1
  86. package/dist/jobs/Jobs.js +12 -10
  87. package/dist/jobs/Jobs.js.map +1 -1
  88. package/dist/observation/ObserverRunningState.d.ts +9 -15
  89. package/dist/observation/ObserverRunningState.d.ts.map +1 -1
  90. package/dist/observation/ObserverRunningState.js +8 -14
  91. package/dist/observation/ObserverRunningState.js.map +1 -1
  92. package/dist/observation/index.d.ts +1 -0
  93. package/dist/observation/index.d.ts.map +1 -1
  94. package/dist/observation/index.js +1 -0
  95. package/dist/observation/index.js.map +1 -1
  96. package/dist/observation/toObserverRunningState.d.ts +9 -0
  97. package/dist/observation/toObserverRunningState.d.ts.map +1 -0
  98. package/dist/observation/toObserverRunningState.js +26 -0
  99. package/dist/observation/toObserverRunningState.js.map +1 -0
  100. package/dist/projections/IProjections.d.ts +71 -1
  101. package/dist/projections/IProjections.d.ts.map +1 -1
  102. package/dist/projections/ProjectionQueryResult.d.ts +8 -0
  103. package/dist/projections/ProjectionQueryResult.d.ts.map +1 -0
  104. package/dist/projections/ProjectionQueryResult.js +4 -0
  105. package/dist/projections/ProjectionQueryResult.js.map +1 -0
  106. package/dist/projections/ProjectionState.d.ts +18 -0
  107. package/dist/projections/ProjectionState.d.ts.map +1 -0
  108. package/dist/projections/ProjectionState.js +4 -0
  109. package/dist/projections/ProjectionState.js.map +1 -0
  110. package/dist/projections/Projections.d.ts +46 -1
  111. package/dist/projections/Projections.d.ts.map +1 -1
  112. package/dist/projections/Projections.js +153 -6
  113. package/dist/projections/Projections.js.map +1 -1
  114. package/dist/projections/UnableToQueryProjection.d.ts +12 -0
  115. package/dist/projections/UnableToQueryProjection.d.ts.map +1 -0
  116. package/dist/projections/UnableToQueryProjection.js +17 -0
  117. package/dist/projections/UnableToQueryProjection.js.map +1 -0
  118. package/dist/projections/index.d.ts +3 -0
  119. package/dist/projections/index.d.ts.map +1 -1
  120. package/dist/projections/index.js +1 -0
  121. package/dist/projections/index.js.map +1 -1
  122. package/dist/projections/modelBound/eventSequence.d.ts +22 -0
  123. package/dist/projections/modelBound/eventSequence.d.ts.map +1 -0
  124. package/dist/projections/modelBound/eventSequence.js +34 -0
  125. package/dist/projections/modelBound/eventSequence.js.map +1 -0
  126. package/dist/projections/modelBound/fromAll.d.ts +26 -0
  127. package/dist/projections/modelBound/fromAll.d.ts.map +1 -0
  128. package/dist/projections/modelBound/fromAll.js +31 -0
  129. package/dist/projections/modelBound/fromAll.js.map +1 -0
  130. package/dist/projections/modelBound/index.d.ts +4 -0
  131. package/dist/projections/modelBound/index.d.ts.map +1 -1
  132. package/dist/projections/modelBound/index.js +3 -0
  133. package/dist/projections/modelBound/index.js.map +1 -1
  134. package/dist/projections/modelBound/noAutoMap.d.ts +26 -0
  135. package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -0
  136. package/dist/projections/modelBound/noAutoMap.js +44 -0
  137. package/dist/projections/modelBound/noAutoMap.js.map +1 -0
  138. package/dist/reactors/Reactors.d.ts.map +1 -1
  139. package/dist/reactors/Reactors.js +7 -3
  140. package/dist/reactors/Reactors.js.map +1 -1
  141. package/dist/readModels/MaterializedReadModels.d.ts +0 -1
  142. package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
  143. package/dist/readModels/MaterializedReadModels.js +2 -8
  144. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  145. package/dist/readModels/ReadModelSubjectResolver.d.ts +22 -0
  146. package/dist/readModels/ReadModelSubjectResolver.d.ts.map +1 -0
  147. package/dist/readModels/ReadModelSubjectResolver.js +38 -0
  148. package/dist/readModels/ReadModelSubjectResolver.js.map +1 -0
  149. package/dist/readModels/ReadModels.d.ts.map +1 -1
  150. package/dist/readModels/ReadModels.js +7 -7
  151. package/dist/readModels/ReadModels.js.map +1 -1
  152. package/dist/readModels/index.d.ts +1 -0
  153. package/dist/readModels/index.d.ts.map +1 -1
  154. package/dist/readModels/index.js +1 -0
  155. package/dist/readModels/index.js.map +1 -1
  156. package/dist/reducers/Reducers.d.ts.map +1 -1
  157. package/dist/reducers/Reducers.js +21 -3
  158. package/dist/reducers/Reducers.js.map +1 -1
  159. package/dist/schemas/JsonSchema.d.ts +14 -0
  160. package/dist/schemas/JsonSchema.d.ts.map +1 -1
  161. package/dist/schemas/JsonSchemaGenerator.d.ts +61 -5
  162. package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
  163. package/dist/schemas/JsonSchemaGenerator.js +130 -26
  164. package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
  165. package/dist/schemas/index.d.ts +1 -1
  166. package/dist/schemas/index.d.ts.map +1 -1
  167. package/dist/seeding/EventSeeding.d.ts.map +1 -1
  168. package/dist/seeding/EventSeeding.js +3 -1
  169. package/dist/seeding/EventSeeding.js.map +1 -1
  170. package/dist/tsconfig.tsbuildinfo +1 -1
  171. package/eventSequences/AppendOptions.ts +9 -0
  172. package/eventSequences/EventForEventSourceId.ts +9 -0
  173. package/eventSequences/EventSequence.ts +20 -6
  174. package/events/EventContext.ts +4 -0
  175. package/events/Tag.ts +14 -0
  176. package/events/constraints/Constraints.ts +3 -3
  177. package/events/filterEventsByTagDecorator.ts +51 -0
  178. package/events/index.ts +4 -0
  179. package/events/mergeTags.spec.ts +50 -0
  180. package/events/mergeTags.ts +27 -0
  181. package/events/tagDecorator.spec.ts +115 -0
  182. package/events/tagDecorator.ts +59 -0
  183. package/jobs/IJobs.ts +4 -4
  184. package/jobs/Jobs.ts +16 -14
  185. package/observation/ObserverRunningState.ts +9 -18
  186. package/observation/index.ts +1 -0
  187. package/observation/toObserverRunningState.ts +27 -0
  188. package/package.json +25 -10
  189. package/projections/IProjections.ts +81 -1
  190. package/projections/ProjectionQueryResult.ts +10 -0
  191. package/projections/ProjectionState.ts +25 -0
  192. package/projections/Projections.childrenAndNested.spec.ts +1 -1
  193. package/projections/Projections.modelBoundCompleteness.spec.ts +224 -0
  194. package/projections/Projections.operationalSurface.spec.ts +257 -0
  195. package/projections/Projections.spec.ts +1 -1
  196. package/projections/Projections.ts +180 -5
  197. package/projections/UnableToQueryProjection.ts +17 -0
  198. package/projections/index.ts +3 -0
  199. package/projections/modelBound/eventSequence.ts +38 -0
  200. package/projections/modelBound/fromAll.ts +42 -0
  201. package/projections/modelBound/index.ts +4 -0
  202. package/projections/modelBound/noAutoMap.ts +47 -0
  203. package/reactors/Reactors.ts +7 -3
  204. package/readModels/MaterializedReadModels.spec.ts +98 -0
  205. package/readModels/MaterializedReadModels.ts +2 -8
  206. package/readModels/ReadModelSubjectResolver.spec.ts +84 -0
  207. package/readModels/ReadModelSubjectResolver.ts +44 -0
  208. package/readModels/ReadModels.spec.ts +118 -0
  209. package/readModels/ReadModels.ts +7 -7
  210. package/readModels/index.ts +1 -0
  211. package/reducers/Reducers.spec.ts +80 -0
  212. package/reducers/Reducers.ts +22 -3
  213. package/schemas/JsonSchema.ts +16 -0
  214. package/schemas/JsonSchemaGenerator.spec.ts +195 -0
  215. package/schemas/JsonSchemaGenerator.ts +145 -27
  216. package/schemas/index.ts +1 -1
  217. package/seeding/EventSeeding.ts +3 -1
@@ -1,6 +1,21 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ /**
5
+ * Represents a single compliance classification recorded on a schema node.
6
+ */
7
+ export interface ComplianceSchemaMetadata {
8
+ /**
9
+ * The type of compliance metadata (e.g. 'PII').
10
+ */
11
+ metadataType: string;
12
+
13
+ /**
14
+ * Any additional details - can be empty.
15
+ */
16
+ details: string;
17
+ }
18
+
4
19
  /**
5
20
  * Represents a JSON Schema object.
6
21
  */
@@ -15,4 +30,5 @@ export type JsonSchema = {
15
30
  items?: JsonSchema;
16
31
  additionalProperties?: boolean | JsonSchema;
17
32
  enum?: Array<string | number | boolean | null>;
33
+ compliance?: ComplianceSchemaMetadata[];
18
34
  };
@@ -0,0 +1,195 @@
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, field } from '@cratis/fundamentals';
6
+ import { describe, expect, it } from 'vitest';
7
+ import { pii } from '../compliance/pii';
8
+ import { eventType, getEventTypeJsonSchemaFor } from '../events/eventTypeDecorator';
9
+ import { getReadModelMetadata, readModel } from '../readModels/readModel';
10
+ import { JsonSchema } from './JsonSchema';
11
+
12
+ // Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
13
+ // fixtures don't depend on the test runner's decorator-syntax support - they exercise exactly
14
+ // the same decorator functions and metadata storage that `@decorator` syntax would invoke.
15
+ //
16
+ // Order matters here in a way it would not for a real `@decorator` stack: class decorators
17
+ // apply bottom-to-top, so `pii()` must run - and therefore be called - before `readModel()`/
18
+ // `eventType()` so the compliance metadata already exists when the schema is generated.
19
+
20
+ function schemaFor(target: Function): JsonSchema {
21
+ return getReadModelMetadata(target)!.schema;
22
+ }
23
+
24
+ describe('JsonSchemaGenerator', () => {
25
+ describe('when a class is marked @pii() at the class level', () => {
26
+ class PersonProfile {
27
+ name = '';
28
+ email = '';
29
+ }
30
+ pii()(PersonProfile);
31
+ readModel()(PersonProfile);
32
+
33
+ const schema = schemaFor(PersonProfile);
34
+
35
+ it('should mark every one of its own properties as PII', () => {
36
+ expect(schema.properties!.name.compliance).toEqual([{ metadataType: 'PII', details: '' }]);
37
+ expect(schema.properties!.email.compliance).toEqual([{ metadataType: 'PII', details: '' }]);
38
+ });
39
+ });
40
+
41
+ describe('when only a single property is marked @pii()', () => {
42
+ class Contact {
43
+ name = '';
44
+ ssn = '';
45
+ }
46
+ pii('Social security number')(Contact.prototype, 'ssn');
47
+ readModel()(Contact);
48
+
49
+ const schema = schemaFor(Contact);
50
+
51
+ it('should mark the decorated property as PII', () => {
52
+ expect(schema.properties!.ssn.compliance).toEqual([{ metadataType: 'PII', details: 'Social security number' }]);
53
+ });
54
+
55
+ it('should leave the other property without compliance metadata', () => {
56
+ expect(schema.properties!.name.compliance).toBeUndefined();
57
+ });
58
+ });
59
+
60
+ describe('when a property is typed as a ConceptAs<T> marked @pii()', () => {
61
+ class EmailAddress extends ConceptAs<string> {
62
+ constructor(value: string) {
63
+ super(value);
64
+ }
65
+ }
66
+ pii('Email address')(EmailAddress);
67
+
68
+ describe('on a read model', () => {
69
+ class Customer {
70
+ email: EmailAddress = new EmailAddress('');
71
+ }
72
+ readModel()(Customer);
73
+
74
+ const schema = schemaFor(Customer);
75
+
76
+ it('should mark the concept-typed property as PII', () => {
77
+ expect(schema.properties!.email.compliance).toEqual([{ metadataType: 'PII', details: 'Email address' }]);
78
+ });
79
+
80
+ it('should describe the concept as its underlying primitive type', () => {
81
+ expect(schema.properties!.email.type).toBe('string');
82
+ });
83
+ });
84
+
85
+ describe('on an event', () => {
86
+ class CustomerRegistered {
87
+ email: EmailAddress = new EmailAddress('');
88
+ }
89
+ eventType()(CustomerRegistered);
90
+
91
+ const schema = getEventTypeJsonSchemaFor(CustomerRegistered);
92
+
93
+ it('should mark the concept-typed property as PII', () => {
94
+ expect(schema.properties!.email.compliance).toEqual([{ metadataType: 'PII', details: 'Email address' }]);
95
+ });
96
+ });
97
+ });
98
+
99
+ describe('when a property-level @pii() combines with a class-level @pii()', () => {
100
+ class Employee {
101
+ name = '';
102
+ email = '';
103
+ }
104
+ pii()(Employee.prototype, 'name');
105
+ pii('Every field on this record is personal')(Employee);
106
+ readModel()(Employee);
107
+
108
+ const schema = schemaFor(Employee);
109
+
110
+ it('should not duplicate the compliance entry on the property carrying both markers', () => {
111
+ expect(schema.properties!.name.compliance).toHaveLength(1);
112
+ });
113
+
114
+ it('should keep the property-level details when both sources apply', () => {
115
+ expect(schema.properties!.name.compliance).toEqual([{ metadataType: 'PII', details: '' }]);
116
+ });
117
+
118
+ it('should still mark the property that only carries the class-level marker', () => {
119
+ expect(schema.properties!.email.compliance).toEqual([{ metadataType: 'PII', details: 'Every field on this record is personal' }]);
120
+ });
121
+ });
122
+
123
+ describe('when a property typed as a nested composite value object is marked @pii()', () => {
124
+ class ContactDetails {
125
+ phone = '';
126
+ fax = '';
127
+ }
128
+ class Vendor {
129
+ contact: ContactDetails = new ContactDetails();
130
+ }
131
+ pii('Vendor contact information')(Vendor.prototype, 'contact');
132
+ readModel()(Vendor);
133
+
134
+ const schema = schemaFor(Vendor);
135
+
136
+ it('should push the compliance metadata down onto every leaf property', () => {
137
+ expect(schema.properties!.contact.properties!.phone.compliance).toEqual([{ metadataType: 'PII', details: 'Vendor contact information' }]);
138
+ expect(schema.properties!.contact.properties!.fax.compliance).toEqual([{ metadataType: 'PII', details: 'Vendor contact information' }]);
139
+ });
140
+
141
+ it('should not leave compliance metadata on the container node itself', () => {
142
+ expect(schema.properties!.contact.compliance).toBeUndefined();
143
+ });
144
+ });
145
+
146
+ describe('when an array element is a ConceptAs<T> marked @pii()', () => {
147
+ class RequirementCode extends ConceptAs<string> {
148
+ constructor(value: string) {
149
+ super(value);
150
+ }
151
+ }
152
+ pii('Sensitive requirement code')(RequirementCode);
153
+
154
+ class Contract {
155
+ codes: RequirementCode[] = [];
156
+ }
157
+ field(Array, { enumerable: true, genericArguments: [RequirementCode] })(Contract.prototype, 'codes');
158
+ readModel()(Contract);
159
+
160
+ const schema = schemaFor(Contract);
161
+
162
+ it('should describe the property as an array', () => {
163
+ expect(schema.properties!.codes.type).toBe('array');
164
+ });
165
+
166
+ it('should carry the element concept compliance metadata onto the item schema', () => {
167
+ expect(schema.properties!.codes.items!.compliance).toEqual([{ metadataType: 'PII', details: 'Sensitive requirement code' }]);
168
+ });
169
+
170
+ it('should describe the item as the concept underlying primitive type', () => {
171
+ expect(schema.properties!.codes.items!.type).toBe('string');
172
+ });
173
+
174
+ it('should not leave coarse compliance metadata on the array container itself', () => {
175
+ expect(schema.properties!.codes.compliance).toBeUndefined();
176
+ });
177
+ });
178
+
179
+ describe('when an array element is not a ConceptAs<T>', () => {
180
+ class PlainItem {
181
+ value = '';
182
+ }
183
+ class Basket {
184
+ items: PlainItem[] = [];
185
+ }
186
+ field(Array, { enumerable: true, genericArguments: [PlainItem] })(Basket.prototype, 'items');
187
+ readModel()(Basket);
188
+
189
+ const schema = schemaFor(Basket);
190
+
191
+ it('should fall back to an opaque object item schema', () => {
192
+ expect(schema.properties!.items.items).toEqual({ type: 'object' });
193
+ });
194
+ });
195
+ });
@@ -2,9 +2,10 @@
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 { ConceptAs, Guid } from '@cratis/fundamentals';
6
- import { JsonSchema } from './JsonSchema';
5
+ import { ConceptAs, Constructor, Fields, Guid } from '@cratis/fundamentals';
6
+ import { ComplianceSchemaMetadata, JsonSchema } from './JsonSchema';
7
7
  import { TypeIntrospector } from '../types';
8
+ import { ComplianceMetadata } from '../compliance/ComplianceMetadata';
8
9
  import { ComplianceMetadataResolver } from '../compliance/ComplianceMetadataResolver';
9
10
 
10
11
  /**
@@ -48,12 +49,17 @@ export class JsonSchemaGenerator {
48
49
  const prototype = target.prototype;
49
50
 
50
51
  for (const [memberName, memberType] of membersToUse.entries()) {
51
- const propertySchema = this.mapRuntimeTypeToSchema(memberType);
52
+ const propertySchema = this.mapRuntimeTypeToSchema(memberType, target, memberName);
52
53
  // Only include properties whose type was resolved. An empty schema ({}) means
53
54
  // the runtime type was unavailable (e.g. esbuild/tsx omits design:paramtypes).
54
55
  if (Object.keys(propertySchema).length > 0) {
55
- // Add compliance metadata to property schema if present (both property and type-level)
56
- this.addComplianceMetadata(propertySchema, prototype, memberName, memberType);
56
+ // An array of concept elements resolves and applies its own item-level compliance
57
+ // inside mapRuntimeTypeToSchema - the general property/type/declaring-class walk
58
+ // below is skipped for it, mirroring the C# generator's enumerable-of-concept branch.
59
+ if (!this.isConceptArrayMember(target, memberName, memberType)) {
60
+ const metadata = this.collectComplianceMetadata(prototype, memberName, memberType);
61
+ this.addComplianceMetadataToSchema(propertySchema, metadata);
62
+ }
57
63
  schemaProperties[memberName] = propertySchema;
58
64
  }
59
65
  }
@@ -72,7 +78,7 @@ export class JsonSchemaGenerator {
72
78
  };
73
79
  }
74
80
 
75
- private static mapRuntimeTypeToSchema(runtimeType: Function | undefined): JsonSchema {
81
+ private static mapRuntimeTypeToSchema(runtimeType: Function | undefined, declaringType?: Function, propertyName?: string): JsonSchema {
76
82
  const knownTypeFormat = this.getKnownTypeFormat(runtimeType);
77
83
  if (knownTypeFormat) {
78
84
  return knownTypeFormat;
@@ -91,7 +97,7 @@ export class JsonSchemaGenerator {
91
97
  }
92
98
 
93
99
  if (runtimeType === Array) {
94
- return { type: 'array', items: { type: 'object' } };
100
+ return this.mapArrayTypeToSchema(declaringType, propertyName);
95
101
  }
96
102
 
97
103
  if (!runtimeType) {
@@ -115,6 +121,63 @@ export class JsonSchemaGenerator {
115
121
  return { type: 'object' };
116
122
  }
117
123
 
124
+ /**
125
+ * Maps an array-typed member to a schema, resolving the element type from a
126
+ * `@field(Array, { genericArguments: [ItemType] })` declaration when present.
127
+ * @param declaringType - The class constructor that declares the array property.
128
+ * @param propertyName - The array property name.
129
+ * @returns The array schema, with the element's own compliance metadata carried onto `items` when the element is a PII concept.
130
+ */
131
+ private static mapArrayTypeToSchema(declaringType: Function | undefined, propertyName: string | undefined): JsonSchema {
132
+ const elementType = this.getArrayElementType(declaringType, propertyName);
133
+
134
+ // An array whose element is a ConceptAs<T> loses its classification the moment it is put in
135
+ // a list unless the element concept's own compliance metadata is carried onto the item schema -
136
+ // a value that would be encrypted as a scalar would otherwise be persisted in the clear as a
137
+ // list element. Mirrors the C# generator's explicit enumerable-of-concept branch.
138
+ if (elementType && this.isConceptAs(elementType)) {
139
+ const itemSchema = this.mapRuntimeTypeToSchema(elementType);
140
+ const metadata = ComplianceMetadataResolver.getMetadataForType(elementType);
141
+ this.addComplianceMetadataToSchema(itemSchema, metadata);
142
+ return { type: 'array', items: itemSchema };
143
+ }
144
+
145
+ return { type: 'array', items: { type: 'object' } };
146
+ }
147
+
148
+ /**
149
+ * Resolves the element type of an array property from its `@field(Array, { genericArguments: [...] })`
150
+ * declaration. TypeScript erases generic type arguments at runtime, so without an explicit
151
+ * `@field` declaration the element type cannot be recovered.
152
+ * @param declaringType - The class constructor that declares the array property.
153
+ * @param propertyName - The array property name.
154
+ * @returns The element type constructor, or undefined when it cannot be resolved.
155
+ */
156
+ private static getArrayElementType(declaringType: Function | undefined, propertyName: string | undefined): Function | undefined {
157
+ if (!declaringType || !propertyName) {
158
+ return undefined;
159
+ }
160
+
161
+ const field = Fields.getFieldsForType(declaringType as Constructor).find(candidate => candidate.name === propertyName);
162
+ return field?.genericArguments?.[0];
163
+ }
164
+
165
+ /**
166
+ * Checks whether a member is an array whose element type is a ConceptAs<T>.
167
+ * @param declaringType - The class constructor that declares the property.
168
+ * @param propertyName - The property name.
169
+ * @param runtimeType - The member's reflected runtime type.
170
+ * @returns True when the member is an array of concept elements; false otherwise.
171
+ */
172
+ private static isConceptArrayMember(declaringType: Function, propertyName: string, runtimeType: Function | undefined): boolean {
173
+ if (runtimeType !== Array) {
174
+ return false;
175
+ }
176
+
177
+ const elementType = this.getArrayElementType(declaringType, propertyName);
178
+ return elementType !== undefined && this.isConceptAs(elementType);
179
+ }
180
+
118
181
  private static isConceptAs(runtimeType: Function): boolean {
119
182
  let current: Function | null = runtimeType;
120
183
  while (current && current !== Function.prototype) {
@@ -150,35 +213,90 @@ export class JsonSchemaGenerator {
150
213
  }
151
214
 
152
215
  /**
153
- * Adds compliance metadata to a property schema if the property has compliance decorators.
154
- * Also checks if the property's type itself is marked as PII (e.g., ConceptAs types).
155
- * @param schema - The property schema to add compliance metadata to.
156
- * @param target - The class prototype.
216
+ * Collects compliance metadata for a property from every source C# resolves PII from: the
217
+ * property itself, its declaring class, and its own type (the concept case).
218
+ * @param target - The class prototype the property is declared on.
157
219
  * @param propertyKey - The property name.
220
+ * @param propertyType - The property's runtime type, when resolved.
221
+ * @returns The collected compliance metadata, in property → declaring-class → type order.
158
222
  */
159
- private static addComplianceMetadata(schema: JsonSchema, target: object, propertyKey: string, propertyType?: Function): void {
160
- const complianceArray: Array<{ metadataType: string; details: string }> = [];
223
+ private static collectComplianceMetadata(target: object, propertyKey: string, propertyType?: Function): ComplianceMetadata[] {
224
+ const metadata: ComplianceMetadata[] = [];
161
225
 
162
- // Check for property-level compliance decorators
226
+ // Property-level compliance decorator.
163
227
  if (ComplianceMetadataResolver.hasMetadataFor(target, propertyKey)) {
164
- const propertyMetadata = ComplianceMetadataResolver.getMetadataFor(target, propertyKey);
165
- complianceArray.push(...propertyMetadata.map(metadata => ({
166
- metadataType: metadata.metadataType.value.toString(),
167
- details: metadata.details
168
- })));
228
+ metadata.push(...ComplianceMetadataResolver.getMetadataFor(target, propertyKey));
229
+ }
230
+
231
+ // Declaring class-level compliance decorator - a class-level @pii() marks every one of
232
+ // its own properties, the same way C#'s PIIMetadataProvider checks property.DeclaringType.
233
+ const declaringClass = (target as { constructor?: Function }).constructor;
234
+ if (declaringClass) {
235
+ metadata.push(...ComplianceMetadataResolver.getMetadataForType(declaringClass));
169
236
  }
170
237
 
171
- // Check for type-level compliance decorators (e.g., @pii on ConceptAs)
238
+ // Type-level compliance decorator on the property's own type (e.g., @pii on a ConceptAs).
172
239
  if (propertyType) {
173
- const typeMetadata = ComplianceMetadataResolver.getMetadataForType(propertyType);
174
- complianceArray.push(...typeMetadata.map(metadata => ({
175
- metadataType: metadata.metadataType.value.toString(),
176
- details: metadata.details
177
- })));
240
+ metadata.push(...ComplianceMetadataResolver.getMetadataForType(propertyType));
241
+ }
242
+
243
+ return metadata;
244
+ }
245
+
246
+ /**
247
+ * Adds compliance metadata to a schema node, descending into an object's properties so that
248
+ * the metadata always lands on the leaves that actually hold a value.
249
+ * @param schema - The schema node to add to.
250
+ * @param metadata - The compliance metadata to add.
251
+ * @remarks
252
+ * A compliance marker can be declared on something that is not a single value: a `@pii()` on a
253
+ * composite value-object type, or on a property whose type is such an object. Compliance is
254
+ * applied per value, so leaving the marker on the container would make Chronicle hand the whole
255
+ * JSON object to the value handler and store one opaque ciphertext string where the schema still
256
+ * says "object". Releasing that gives back a string, not an object, and the read model then
257
+ * fails to materialize. Pushing the metadata down to every leaf keeps encryption symmetric with
258
+ * the release walk, keeps each value independently encrypted, and preserves the document shape.
259
+ *
260
+ * An array-typed node is deliberately left as a container: coarse compliance on a whole
261
+ * collection is an established, separately handled behavior (the collection is blob-encrypted
262
+ * and its shape restored on release).
263
+ */
264
+ private static addComplianceMetadataToSchema(schema: JsonSchema, metadata: ComplianceMetadata[]): void {
265
+ if (metadata.length === 0) {
266
+ return;
178
267
  }
179
268
 
180
- if (complianceArray.length > 0) {
181
- (schema as Record<string, unknown>).compliance = complianceArray;
269
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
270
+ for (const propertySchema of Object.values(schema.properties)) {
271
+ this.addComplianceMetadataToSchema(propertySchema, metadata);
272
+ }
273
+ return;
182
274
  }
275
+
276
+ const compliance = schema.compliance ?? [];
277
+ for (const item of metadata) {
278
+ const metadataType = item.metadataType.value.toString();
279
+ if (!this.hasComplianceMetadataOfType(compliance, metadataType)) {
280
+ compliance.push({ metadataType, details: item.details });
281
+ }
282
+ }
283
+
284
+ if (compliance.length > 0) {
285
+ schema.compliance = compliance;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Checks whether a compliance array already carries metadata of a given type.
291
+ * @param compliance - The compliance array to check.
292
+ * @param metadataType - The metadata type to look for.
293
+ * @returns True when the metadata type is already present, false if not.
294
+ * @remarks
295
+ * A leaf can be reached by more than one marker — for example a `@pii()` concept inside a value
296
+ * object whose type is itself marked `@pii()`. Recording the same metadata type twice adds
297
+ * nothing and makes the generated schema noisier to read and to diff.
298
+ */
299
+ private static hasComplianceMetadataOfType(compliance: ComplianceSchemaMetadata[], metadataType: string): boolean {
300
+ return compliance.some(item => item.metadataType === metadataType);
183
301
  }
184
302
  }
package/schemas/index.ts CHANGED
@@ -3,4 +3,4 @@
3
3
 
4
4
  export { JsonSchemaGenerator } from './JsonSchemaGenerator';
5
5
  export { jsonSchemaProperty, getTrackedJsonSchemaProperties } from './jsonSchemaProperty';
6
- export type { JsonSchema } from './JsonSchema';
6
+ export type { ComplianceSchemaMetadata, JsonSchema } from './JsonSchema';
@@ -6,6 +6,7 @@ import { IClientArtifactsProvider } from '../artifacts';
6
6
  import { ChronicleConnection } from '../connection';
7
7
  import { EventStoreNamespaceName } from '../EventStoreNamespaceName';
8
8
  import { getEventTypeFor } from '../events/eventTypeDecorator';
9
+ import { getTagsFor } from '../events/tagDecorator';
9
10
  import { ICanSeedEvents } from './ICanSeedEvents';
10
11
  import { IEventSeeding } from './IEventSeeding';
11
12
  import { IEventSeedingBuilder } from './IEventSeedingBuilder';
@@ -104,11 +105,12 @@ export class EventSeeding implements IEventSeeding {
104
105
  private addEntries<TEvent extends object>(eventSourceId: string, events: Iterable<TEvent>, isGlobal: boolean, targetNamespace: string): void {
105
106
  for (const event of events) {
106
107
  const eventType = getEventTypeFor(event.constructor as Function);
108
+ const tags = getTagsFor(event.constructor as Function).map(t => t.value);
107
109
  this._entries.push({
108
110
  eventSourceId,
109
111
  eventTypeId: eventType.id.value,
110
112
  content: JSON.stringify(event),
111
- tags: [],
113
+ tags,
112
114
  isGlobal,
113
115
  targetNamespace
114
116
  });