@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.
- package/ChronicleClient.ts +4 -3
- package/EventStore.ts +4 -3
- package/README.md +40 -28
- package/compliance/ComplianceContracts.ts +59 -0
- package/compliance/IPIIManager.ts +12 -0
- package/compliance/PIIManager.spec.ts +50 -0
- package/compliance/PIIManager.ts +9 -0
- package/compliance/PIINotSupportedOnEventSourceId.ts +21 -0
- package/compliance/index.ts +2 -0
- package/compliance/pii.spec.ts +37 -0
- package/compliance/pii.ts +16 -1
- package/compliance/subject.spec.ts +45 -0
- package/compliance/subject.ts +64 -0
- package/connection/callResults.spec.ts +90 -0
- package/connection/callResults.ts +112 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +4 -3
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +4 -3
- package/dist/EventStore.js.map +1 -1
- package/dist/compliance/ComplianceContracts.d.ts +31 -0
- package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
- package/dist/compliance/ComplianceContracts.js +52 -0
- package/dist/compliance/ComplianceContracts.js.map +1 -1
- package/dist/compliance/IPIIManager.d.ts +11 -0
- package/dist/compliance/IPIIManager.d.ts.map +1 -1
- package/dist/compliance/PIIManager.d.ts +2 -0
- package/dist/compliance/PIIManager.d.ts.map +1 -1
- package/dist/compliance/PIIManager.js +8 -0
- package/dist/compliance/PIIManager.js.map +1 -1
- package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts +11 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts.map +1 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.js +19 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.js.map +1 -0
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.d.ts.map +1 -1
- package/dist/compliance/index.js +2 -0
- package/dist/compliance/index.js.map +1 -1
- package/dist/compliance/pii.d.ts.map +1 -1
- package/dist/compliance/pii.js +13 -1
- package/dist/compliance/pii.js.map +1 -1
- package/dist/compliance/subject.d.ts +39 -0
- package/dist/compliance/subject.d.ts.map +1 -0
- package/dist/compliance/subject.js +58 -0
- package/dist/compliance/subject.js.map +1 -0
- package/dist/connection/callResults.d.ts +69 -0
- package/dist/connection/callResults.d.ts.map +1 -0
- package/dist/connection/callResults.js +74 -0
- package/dist/connection/callResults.js.map +1 -0
- package/dist/eventSequences/AppendOptions.d.ts +8 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +7 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +17 -6
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/events/EventContext.d.ts +3 -0
- package/dist/events/EventContext.d.ts.map +1 -1
- package/dist/events/Tag.d.ts +10 -0
- package/dist/events/Tag.d.ts.map +1 -0
- package/dist/events/Tag.js +16 -0
- package/dist/events/Tag.js.map +1 -0
- package/dist/events/constraints/Constraints.js +3 -3
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/events/filterEventsByTagDecorator.d.ts +35 -0
- package/dist/events/filterEventsByTagDecorator.d.ts.map +1 -0
- package/dist/events/filterEventsByTagDecorator.js +48 -0
- package/dist/events/filterEventsByTagDecorator.js.map +1 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +4 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/mergeTags.d.ts +12 -0
- package/dist/events/mergeTags.d.ts.map +1 -0
- package/dist/events/mergeTags.js +26 -0
- package/dist/events/mergeTags.js.map +1 -0
- package/dist/events/tagDecorator.d.ts +38 -0
- package/dist/events/tagDecorator.d.ts.map +1 -0
- package/dist/events/tagDecorator.js +54 -0
- package/dist/events/tagDecorator.js.map +1 -0
- package/dist/jobs/IJobs.d.ts +4 -4
- package/dist/jobs/IJobs.d.ts.map +1 -1
- package/dist/jobs/Jobs.d.ts +4 -4
- package/dist/jobs/Jobs.d.ts.map +1 -1
- package/dist/jobs/Jobs.js +12 -10
- package/dist/jobs/Jobs.js.map +1 -1
- package/dist/observation/ObserverRunningState.d.ts +9 -15
- package/dist/observation/ObserverRunningState.d.ts.map +1 -1
- package/dist/observation/ObserverRunningState.js +8 -14
- package/dist/observation/ObserverRunningState.js.map +1 -1
- package/dist/observation/index.d.ts +1 -0
- package/dist/observation/index.d.ts.map +1 -1
- package/dist/observation/index.js +1 -0
- package/dist/observation/index.js.map +1 -1
- package/dist/observation/toObserverRunningState.d.ts +9 -0
- package/dist/observation/toObserverRunningState.d.ts.map +1 -0
- package/dist/observation/toObserverRunningState.js +26 -0
- package/dist/observation/toObserverRunningState.js.map +1 -0
- package/dist/projections/IProjections.d.ts +71 -1
- package/dist/projections/IProjections.d.ts.map +1 -1
- package/dist/projections/ProjectionQueryResult.d.ts +8 -0
- package/dist/projections/ProjectionQueryResult.d.ts.map +1 -0
- package/dist/projections/ProjectionQueryResult.js +4 -0
- package/dist/projections/ProjectionQueryResult.js.map +1 -0
- package/dist/projections/ProjectionState.d.ts +18 -0
- package/dist/projections/ProjectionState.d.ts.map +1 -0
- package/dist/projections/ProjectionState.js +4 -0
- package/dist/projections/ProjectionState.js.map +1 -0
- package/dist/projections/Projections.d.ts +46 -1
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +153 -6
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/UnableToQueryProjection.d.ts +12 -0
- package/dist/projections/UnableToQueryProjection.d.ts.map +1 -0
- package/dist/projections/UnableToQueryProjection.js +17 -0
- package/dist/projections/UnableToQueryProjection.js.map +1 -0
- package/dist/projections/index.d.ts +3 -0
- package/dist/projections/index.d.ts.map +1 -1
- package/dist/projections/index.js +1 -0
- package/dist/projections/index.js.map +1 -1
- package/dist/projections/modelBound/eventSequence.d.ts +22 -0
- package/dist/projections/modelBound/eventSequence.d.ts.map +1 -0
- package/dist/projections/modelBound/eventSequence.js +34 -0
- package/dist/projections/modelBound/eventSequence.js.map +1 -0
- package/dist/projections/modelBound/fromAll.d.ts +26 -0
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -0
- package/dist/projections/modelBound/fromAll.js +31 -0
- package/dist/projections/modelBound/fromAll.js.map +1 -0
- package/dist/projections/modelBound/index.d.ts +4 -0
- package/dist/projections/modelBound/index.d.ts.map +1 -1
- package/dist/projections/modelBound/index.js +3 -0
- package/dist/projections/modelBound/index.js.map +1 -1
- package/dist/projections/modelBound/noAutoMap.d.ts +26 -0
- package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -0
- package/dist/projections/modelBound/noAutoMap.js +44 -0
- package/dist/projections/modelBound/noAutoMap.js.map +1 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +7 -3
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/readModels/MaterializedReadModels.d.ts +0 -1
- package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
- package/dist/readModels/MaterializedReadModels.js +2 -8
- package/dist/readModels/MaterializedReadModels.js.map +1 -1
- package/dist/readModels/ReadModelSubjectResolver.d.ts +22 -0
- package/dist/readModels/ReadModelSubjectResolver.d.ts.map +1 -0
- package/dist/readModels/ReadModelSubjectResolver.js +38 -0
- package/dist/readModels/ReadModelSubjectResolver.js.map +1 -0
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +7 -7
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/index.d.ts +1 -0
- package/dist/readModels/index.d.ts.map +1 -1
- package/dist/readModels/index.js +1 -0
- package/dist/readModels/index.js.map +1 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +21 -3
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/schemas/JsonSchema.d.ts +14 -0
- package/dist/schemas/JsonSchema.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts +61 -5
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +130 -26
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.js +3 -1
- package/dist/seeding/EventSeeding.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOptions.ts +9 -0
- package/eventSequences/EventForEventSourceId.ts +9 -0
- package/eventSequences/EventSequence.ts +20 -6
- package/events/EventContext.ts +4 -0
- package/events/Tag.ts +14 -0
- package/events/constraints/Constraints.ts +3 -3
- package/events/filterEventsByTagDecorator.ts +51 -0
- package/events/index.ts +4 -0
- package/events/mergeTags.spec.ts +50 -0
- package/events/mergeTags.ts +27 -0
- package/events/tagDecorator.spec.ts +115 -0
- package/events/tagDecorator.ts +59 -0
- package/jobs/IJobs.ts +4 -4
- package/jobs/Jobs.ts +16 -14
- package/observation/ObserverRunningState.ts +9 -18
- package/observation/index.ts +1 -0
- package/observation/toObserverRunningState.ts +27 -0
- package/package.json +25 -10
- package/projections/IProjections.ts +81 -1
- package/projections/ProjectionQueryResult.ts +10 -0
- package/projections/ProjectionState.ts +25 -0
- package/projections/Projections.childrenAndNested.spec.ts +1 -1
- package/projections/Projections.modelBoundCompleteness.spec.ts +224 -0
- package/projections/Projections.operationalSurface.spec.ts +257 -0
- package/projections/Projections.spec.ts +1 -1
- package/projections/Projections.ts +180 -5
- package/projections/UnableToQueryProjection.ts +17 -0
- package/projections/index.ts +3 -0
- package/projections/modelBound/eventSequence.ts +38 -0
- package/projections/modelBound/fromAll.ts +42 -0
- package/projections/modelBound/index.ts +4 -0
- package/projections/modelBound/noAutoMap.ts +47 -0
- package/reactors/Reactors.ts +7 -3
- package/readModels/MaterializedReadModels.spec.ts +98 -0
- package/readModels/MaterializedReadModels.ts +2 -8
- package/readModels/ReadModelSubjectResolver.spec.ts +84 -0
- package/readModels/ReadModelSubjectResolver.ts +44 -0
- package/readModels/ReadModels.spec.ts +118 -0
- package/readModels/ReadModels.ts +7 -7
- package/readModels/index.ts +1 -0
- package/reducers/Reducers.spec.ts +80 -0
- package/reducers/Reducers.ts +22 -3
- package/schemas/JsonSchema.ts +16 -0
- package/schemas/JsonSchemaGenerator.spec.ts +195 -0
- package/schemas/JsonSchemaGenerator.ts +145 -27
- package/schemas/index.ts +1 -1
- package/seeding/EventSeeding.ts +3 -1
package/schemas/JsonSchema.ts
CHANGED
|
@@ -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
|
-
//
|
|
56
|
-
|
|
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
|
|
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
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* @param
|
|
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
|
|
160
|
-
const
|
|
223
|
+
private static collectComplianceMetadata(target: object, propertyKey: string, propertyType?: Function): ComplianceMetadata[] {
|
|
224
|
+
const metadata: ComplianceMetadata[] = [];
|
|
161
225
|
|
|
162
|
-
//
|
|
226
|
+
// Property-level compliance decorator.
|
|
163
227
|
if (ComplianceMetadataResolver.hasMetadataFor(target, propertyKey)) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
//
|
|
238
|
+
// Type-level compliance decorator on the property's own type (e.g., @pii on a ConceptAs).
|
|
172
239
|
if (propertyType) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 (
|
|
181
|
-
(
|
|
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';
|
package/seeding/EventSeeding.ts
CHANGED
|
@@ -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
|
});
|