@cratis/chronicle 6.7.0 → 6.8.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/EventStore.ts +10 -4
- package/README.md +16 -5
- package/artifacts/validateArtifactSchemas.spec.ts +3 -3
- package/compliance/pii.ts +2 -2
- package/compliance/subject.ts +1 -1
- package/confidentiality/encrypted.ts +1 -1
- package/dist/EventStore.d.ts +1 -0
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +7 -4
- package/dist/EventStore.js.map +1 -1
- package/dist/compliance/pii.d.ts +2 -2
- package/dist/compliance/pii.js +2 -2
- package/dist/compliance/subject.d.ts +1 -1
- package/dist/compliance/subject.js +1 -1
- package/dist/confidentiality/encrypted.d.ts +1 -1
- package/dist/confidentiality/encrypted.js +1 -1
- package/dist/eventSequences/EventLog.d.ts +2 -1
- package/dist/eventSequences/EventLog.d.ts.map +1 -1
- package/dist/eventSequences/EventLog.js +2 -2
- package/dist/eventSequences/EventLog.js.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +3 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +11 -6
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/events/constraints/ConstraintBuilder.d.ts +2 -0
- package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
- package/dist/events/constraints/Constraints.d.ts +8 -3
- package/dist/events/constraints/Constraints.d.ts.map +1 -1
- package/dist/events/constraints/Constraints.js +147 -6
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/events/constraints/index.d.ts +2 -0
- package/dist/events/constraints/index.d.ts.map +1 -1
- package/dist/events/constraints/index.js +2 -0
- package/dist/events/constraints/index.js.map +1 -1
- package/dist/events/constraints/removeConstraint.d.ts +11 -0
- package/dist/events/constraints/removeConstraint.d.ts.map +1 -0
- package/dist/events/constraints/removeConstraint.js +24 -0
- package/dist/events/constraints/removeConstraint.js.map +1 -0
- package/dist/events/constraints/unique.d.ts +21 -0
- package/dist/events/constraints/unique.d.ts.map +1 -0
- package/dist/events/constraints/unique.js +37 -0
- package/dist/events/constraints/unique.js.map +1 -0
- package/dist/projections/Projections.js +1 -1
- package/dist/projections/Projections.js.map +1 -1
- package/dist/readModels/ReadModels.js +1 -1
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/reducers/Reducers.js +1 -1
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/TypeDiscoverer.js +1 -1
- package/dist/types/TypeDiscoverer.js.map +1 -1
- package/eventSequences/EventLog.ts +4 -2
- package/eventSequences/EventSequence.ts +10 -6
- package/events/classDecorators.standard.spec.ts +45 -1
- package/events/constraints/ConstraintBuilder.ts +2 -0
- package/events/constraints/Constraints.ts +140 -7
- package/events/constraints/for_Constraints/when_combining_decorators_with_fluent_constraints.spec.ts +142 -0
- package/events/constraints/for_Constraints/when_inheriting_removal_decorators.legacy.spec.ts +36 -0
- package/events/constraints/for_Constraints/when_registering_decorated_constraints.spec.ts +63 -0
- package/events/constraints/for_Constraints/when_registering_decorated_event_types.spec.ts +90 -0
- package/events/constraints/for_Constraints/when_rejecting_incompatible_unique_merges.spec.ts +108 -0
- package/events/constraints/for_Constraints/when_resolving_violation_messages.spec.ts +92 -0
- package/events/constraints/index.ts +2 -0
- package/events/constraints/removeConstraint.ts +28 -0
- package/events/constraints/unique.ts +45 -0
- package/package.json +1 -1
- package/projections/Projections.childrenAndNested.spec.ts +0 -2
- package/projections/Projections.modelBoundCompleteness.spec.ts +0 -7
- package/projections/Projections.operationalSurface.spec.ts +0 -4
- package/projections/Projections.spec.ts +0 -2
- package/projections/Projections.ts +1 -1
- package/projections/Projections.variants.spec.ts +0 -5
- package/readModels/MaterializedReadModels.spec.ts +0 -5
- package/readModels/ReadModels.spec.ts +0 -9
- package/readModels/ReadModels.ts +1 -1
- package/readModels/discovery.spec.ts +22 -3
- package/readModels/standardFallbacks.standard.spec.ts +1 -1
- package/reducers/Reducers.spec.ts +1 -1
- package/reducers/Reducers.ts +1 -1
- package/schemas/JsonSchemaGenerator.confidentiality.spec.ts +5 -14
- package/schemas/JsonSchemaGenerator.spec.ts +3 -13
- package/schemas/decorators.legacy.spec.ts +2 -3
- package/schemas/decorators.standard.spec.ts +12 -10
- package/types/TypeDiscoverer.ts +1 -1
|
@@ -7,7 +7,6 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
7
7
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
8
8
|
import { ChronicleConnection } from '../connection/index.js';
|
|
9
9
|
import { eventType } from '../events/eventTypeDecorator.js';
|
|
10
|
-
import { readModel } from '../readModels/readModel.js';
|
|
11
10
|
import { clearWith } from './modelBound/clearWith.js';
|
|
12
11
|
import { eventLog, eventSequence } from './modelBound/eventSequence.js';
|
|
13
12
|
import { fromAll } from './modelBound/fromAll.js';
|
|
@@ -40,7 +39,6 @@ setFrom(MbWorkArrangementSet, 'location')(MbAssignmentSummary.prototype, 'locati
|
|
|
40
39
|
noAutoMap(MbAssignmentSummary.prototype, 'location');
|
|
41
40
|
setFrom(MbCandidateSubmitted, 'name')(MbAssignmentSummary.prototype, 'candidateName');
|
|
42
41
|
fromEvent(MbWorkArrangementSet)(MbAssignmentSummary);
|
|
43
|
-
readModel()(MbAssignmentSummary);
|
|
44
42
|
|
|
45
43
|
class MbFullyExcluded {
|
|
46
44
|
id!: string;
|
|
@@ -49,7 +47,6 @@ class MbFullyExcluded {
|
|
|
49
47
|
setFrom(MbWorkArrangementSet, 'location')(MbFullyExcluded.prototype, 'location');
|
|
50
48
|
fromEvent(MbWorkArrangementSet)(MbFullyExcluded);
|
|
51
49
|
noAutoMap(MbFullyExcluded);
|
|
52
|
-
readModel()(MbFullyExcluded);
|
|
53
50
|
|
|
54
51
|
class MbProductRenamed {
|
|
55
52
|
name!: string;
|
|
@@ -72,7 +69,6 @@ class MbProductVersion {
|
|
|
72
69
|
fromAll('version')(MbProductVersion.prototype, 'version');
|
|
73
70
|
fromEvent(MbProductRenamed)(MbProductVersion);
|
|
74
71
|
fromEvent(MbProductPriceChanged)(MbProductVersion);
|
|
75
|
-
readModel()(MbProductVersion);
|
|
76
72
|
|
|
77
73
|
class MbOrderPlaced {
|
|
78
74
|
amount!: number;
|
|
@@ -86,7 +82,6 @@ class MbOrderSummaryWithCustomSequence {
|
|
|
86
82
|
setFrom(MbOrderPlaced, 'amount')(MbOrderSummaryWithCustomSequence.prototype, 'totalAmount');
|
|
87
83
|
fromEvent(MbOrderPlaced)(MbOrderSummaryWithCustomSequence);
|
|
88
84
|
eventSequence('custom-sequence')(MbOrderSummaryWithCustomSequence);
|
|
89
|
-
readModel()(MbOrderSummaryWithCustomSequence);
|
|
90
85
|
|
|
91
86
|
class MbLocalEvent {
|
|
92
87
|
data!: string;
|
|
@@ -100,7 +95,6 @@ class MbLocalSnapshot {
|
|
|
100
95
|
setFrom(MbLocalEvent, 'data')(MbLocalSnapshot.prototype, 'data');
|
|
101
96
|
fromEvent(MbLocalEvent)(MbLocalSnapshot);
|
|
102
97
|
eventLog(MbLocalSnapshot);
|
|
103
|
-
readModel()(MbLocalSnapshot);
|
|
104
98
|
|
|
105
99
|
class MbProjectNoted {
|
|
106
100
|
note!: string;
|
|
@@ -117,7 +111,6 @@ class MbProjectNotes {
|
|
|
117
111
|
setFrom(MbProjectNoted, 'note')(MbProjectNotes.prototype, 'note');
|
|
118
112
|
clearWith(MbProjectNoteCleared)(MbProjectNotes.prototype, 'note');
|
|
119
113
|
fromEvent(MbProjectNoted)(MbProjectNotes);
|
|
120
|
-
readModel()(MbProjectNotes);
|
|
121
114
|
|
|
122
115
|
interface BuiltFromEntry {
|
|
123
116
|
Key: { Id: string };
|
|
@@ -7,7 +7,6 @@ import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
|
7
7
|
import { ChronicleConnection } from '../connection/index.js';
|
|
8
8
|
import { ObserverRunningState } from '../observation/ObserverRunningState.js';
|
|
9
9
|
import { eventType } from '../events/eventTypeDecorator.js';
|
|
10
|
-
import { readModel } from '../readModels/readModel.js';
|
|
11
10
|
import { eventSequence } from './modelBound/eventSequence.js';
|
|
12
11
|
import { fromEvent } from './modelBound/fromEvent.js';
|
|
13
12
|
import { projection } from './declarative/projection.js';
|
|
@@ -30,17 +29,14 @@ class OpSummary {
|
|
|
30
29
|
}
|
|
31
30
|
fromEvent(OpStateChanged)(OpSummary);
|
|
32
31
|
eventSequence('custom-op-sequence')(OpSummary);
|
|
33
|
-
readModel()(OpSummary);
|
|
34
32
|
|
|
35
33
|
class UndiscoveredSummary {
|
|
36
34
|
id!: string;
|
|
37
35
|
}
|
|
38
|
-
readModel()(UndiscoveredSummary);
|
|
39
36
|
|
|
40
37
|
class DeclarativeReadModel {
|
|
41
38
|
id!: string;
|
|
42
39
|
}
|
|
43
|
-
readModel()(DeclarativeReadModel);
|
|
44
40
|
|
|
45
41
|
class DeclarativeSummaryProjection implements IProjectionFor<DeclarativeReadModel> {
|
|
46
42
|
// discover() never calls define() - only register() does - so a no-op body is sufficient here.
|
|
@@ -6,7 +6,6 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
6
6
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
7
7
|
import { ChronicleConnection } from '../connection/index.js';
|
|
8
8
|
import { eventType } from '../events/eventTypeDecorator.js';
|
|
9
|
-
import { readModel } from '../readModels/readModel.js';
|
|
10
9
|
import { addFrom } from './modelBound/addFrom.js';
|
|
11
10
|
import { count } from './modelBound/count.js';
|
|
12
11
|
import { decrement } from './modelBound/decrement.js';
|
|
@@ -59,7 +58,6 @@ decrement(CounterDecremented)(Inventory.prototype, 'decrementedCount');
|
|
|
59
58
|
count(ThingHappened)(Inventory.prototype, 'thingsHappenedCount');
|
|
60
59
|
count(KeyedThingHappened, 'all-things')(Inventory.prototype, 'constantKeyedCount');
|
|
61
60
|
fromEvent(ItemAdded)(Inventory);
|
|
62
|
-
readModel()(Inventory);
|
|
63
61
|
|
|
64
62
|
/**
|
|
65
63
|
* Builds a {@link Projections} instance wired to capture the registration payload sent
|
|
@@ -601,7 +601,7 @@ export class Projections implements IProjections {
|
|
|
601
601
|
}
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
const memberNames = new Set(getReadModelMetadata(type)?.members.keys() ??
|
|
604
|
+
const memberNames = new Set(getReadModelMetadata(type)?.members.keys() ?? TypeIntrospector.getMembers(type).keys());
|
|
605
605
|
const merged = mergeGlobalHandlers(type.name, memberNames, from, globalHandlers);
|
|
606
606
|
const reclassified = reclassify(type.name, merged, join, enteringEventTypes, variantMetadata.key);
|
|
607
607
|
from = reclassified.from;
|
|
@@ -6,7 +6,6 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
6
6
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
7
7
|
import { ChronicleConnection } from '../connection/index.js';
|
|
8
8
|
import { eventType } from '../events/eventTypeDecorator.js';
|
|
9
|
-
import { readModel } from '../readModels/readModel.js';
|
|
10
9
|
import { entersOn } from './modelBound/entersOn.js';
|
|
11
10
|
import { fromEvent } from './modelBound/fromEvent.js';
|
|
12
11
|
import { globalFor } from './modelBound/globalFor.js';
|
|
@@ -50,7 +49,6 @@ setFrom(IssueCreated, 'title')(BacklogItem.prototype, 'title');
|
|
|
50
49
|
variantOf(WorkItem, 'id')(BacklogItem);
|
|
51
50
|
entersOn(IssueCreated)(BacklogItem);
|
|
52
51
|
fromEvent(IssueCreated)(BacklogItem);
|
|
53
|
-
readModel()(BacklogItem);
|
|
54
52
|
|
|
55
53
|
class PullRequestItem {
|
|
56
54
|
id!: string;
|
|
@@ -64,7 +62,6 @@ variantOf(WorkItem, 'id')(PullRequestItem);
|
|
|
64
62
|
entersOn(PullRequestCreated)(PullRequestItem);
|
|
65
63
|
fromEvent(PullRequestCreated)(PullRequestItem);
|
|
66
64
|
fromEvent(BuildCompleted)(PullRequestItem);
|
|
67
|
-
readModel()(PullRequestItem);
|
|
68
65
|
|
|
69
66
|
/** A shared handler with no entering event of its own - never registered as its own projection. */
|
|
70
67
|
class TitleHandler {
|
|
@@ -81,7 +78,6 @@ class UndeclaredVariant {
|
|
|
81
78
|
setFrom(IssueCreated, 'title')(UndeclaredVariant.prototype, 'title');
|
|
82
79
|
variantOf(WorkItem, 'id')(UndeclaredVariant);
|
|
83
80
|
fromEvent(IssueCreated)(UndeclaredVariant);
|
|
84
|
-
readModel()(UndeclaredVariant);
|
|
85
81
|
|
|
86
82
|
/** A separate identity, used to prove sibling cross-wiring stays scoped to one identity. */
|
|
87
83
|
class OtherIdentity {}
|
|
@@ -92,7 +88,6 @@ class OtherVariant {
|
|
|
92
88
|
variantOf(OtherIdentity, 'id')(OtherVariant);
|
|
93
89
|
entersOn(IssueCreated)(OtherVariant);
|
|
94
90
|
fromEvent(IssueCreated)(OtherVariant);
|
|
95
|
-
readModel()(OtherVariant);
|
|
96
91
|
|
|
97
92
|
interface CapturedDefinition {
|
|
98
93
|
Identifier: string;
|
|
@@ -7,7 +7,6 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
7
7
|
import type { ChronicleConnection } from '../connection/index.js';
|
|
8
8
|
import { pii } from '../compliance/pii.js';
|
|
9
9
|
import { subject } from '../compliance/subject.js';
|
|
10
|
-
import { readModel } from './readModel.js';
|
|
11
10
|
import { MaterializedReadModels } from './MaterializedReadModels.js';
|
|
12
11
|
|
|
13
12
|
// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
|
|
@@ -37,7 +36,6 @@ describe('MaterializedReadModels', () => {
|
|
|
37
36
|
field(String)(Employee.prototype, 'ssn');
|
|
38
37
|
pii()(Employee.prototype, 'ssn');
|
|
39
38
|
subject()(Employee.prototype, 'personId');
|
|
40
|
-
readModel('EmployeeWithSubjectMaterialized')(Employee);
|
|
41
39
|
|
|
42
40
|
it('should release using the decorated property as the subject', async () => {
|
|
43
41
|
const json = JSON.stringify({ id: 'employee-1', personId: 'person-42', ssn: '123-45-6789' });
|
|
@@ -57,7 +55,6 @@ describe('MaterializedReadModels', () => {
|
|
|
57
55
|
field(String)(Customer.prototype, 'id');
|
|
58
56
|
field(String)(Customer.prototype, 'ssn');
|
|
59
57
|
pii()(Customer.prototype, 'ssn');
|
|
60
|
-
readModel('CustomerWithIdOnlyMaterialized')(Customer);
|
|
61
58
|
|
|
62
59
|
it('should fall back to the id property as the subject, unchanged from today', async () => {
|
|
63
60
|
const json = JSON.stringify({ id: 'customer-7', ssn: '987-65-4321' });
|
|
@@ -75,7 +72,6 @@ describe('MaterializedReadModels', () => {
|
|
|
75
72
|
}
|
|
76
73
|
field(String)(Anonymous.prototype, 'ssn');
|
|
77
74
|
pii()(Anonymous.prototype, 'ssn');
|
|
78
|
-
readModel('AnonymousMaterialized')(Anonymous);
|
|
79
75
|
|
|
80
76
|
it('should fail closed without a subject', async () => {
|
|
81
77
|
const json = JSON.stringify({ ssn: '000-00-0000' });
|
|
@@ -98,7 +94,6 @@ describe('MaterializedReadModels', () => {
|
|
|
98
94
|
field(String)(PrivateCustomer.prototype, 'id');
|
|
99
95
|
field(String)(PrivateCustomer.prototype, 'ssn');
|
|
100
96
|
pii()(PrivateCustomer.prototype, 'ssn');
|
|
101
|
-
readModel('PrivateCustomer')(PrivateCustomer);
|
|
102
97
|
|
|
103
98
|
it('should reject instead of returning encrypted content', async () => {
|
|
104
99
|
const { readModels } = createMaterializedReadModels(
|
|
@@ -10,7 +10,6 @@ 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 { readModel } from './readModel.js';
|
|
14
13
|
import { ReadModels } from './ReadModels.js';
|
|
15
14
|
|
|
16
15
|
// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
|
|
@@ -49,7 +48,6 @@ describe('ReadModels', () => {
|
|
|
49
48
|
describe('when no read model instance exists for a key', () => {
|
|
50
49
|
class MissingModel { id = ''; }
|
|
51
50
|
fromEvent(SomeEvent)(MissingModel);
|
|
52
|
-
readModel('MissingModel')(MissingModel);
|
|
53
51
|
|
|
54
52
|
it('should retain the legacy prototype-only instance for getInstanceById', async () => {
|
|
55
53
|
const { readModels, release } = createReadModels(MissingModel);
|
|
@@ -80,7 +78,6 @@ describe('ReadModels', () => {
|
|
|
80
78
|
class ExistingModel { id = ''; }
|
|
81
79
|
field(String)(ExistingModel.prototype, 'id');
|
|
82
80
|
fromEvent(SomeEvent)(ExistingModel);
|
|
83
|
-
readModel('ExistingModel')(ExistingModel);
|
|
84
81
|
|
|
85
82
|
it('should deserialize the model', async () => {
|
|
86
83
|
const { readModels, getInstanceByKey } = createReadModels(ExistingModel);
|
|
@@ -99,7 +96,6 @@ describe('ReadModels', () => {
|
|
|
99
96
|
field(String)(PrivateModel.prototype, 'id');
|
|
100
97
|
field(String)(PrivateModel.prototype, 'ssn');
|
|
101
98
|
pii()(PrivateModel.prototype, 'ssn');
|
|
102
|
-
readModel('PrivateInstanceModel')(PrivateModel);
|
|
103
99
|
class PrivateReducer {}
|
|
104
100
|
reducer('PrivateInstanceReducer', undefined, PrivateModel)(PrivateReducer);
|
|
105
101
|
|
|
@@ -124,7 +120,6 @@ describe('ReadModels', () => {
|
|
|
124
120
|
field(String)(PrivateModel.prototype, 'id');
|
|
125
121
|
field(String)(PrivateModel.prototype, 'ssn');
|
|
126
122
|
pii()(PrivateModel.prototype, 'ssn');
|
|
127
|
-
readModel('PrivateReducerModel')(PrivateModel);
|
|
128
123
|
class PrivateReducer {}
|
|
129
124
|
reducer('PrivateReducer', undefined, PrivateModel)(PrivateReducer);
|
|
130
125
|
|
|
@@ -150,7 +145,6 @@ describe('ReadModels', () => {
|
|
|
150
145
|
}
|
|
151
146
|
subject()(Employee.prototype, 'personId');
|
|
152
147
|
fromEvent(SomeEvent)(Employee);
|
|
153
|
-
readModel('EmployeeWithSubject')(Employee);
|
|
154
148
|
|
|
155
149
|
it('should release using the decorated property as the subject', async () => {
|
|
156
150
|
const { readModels, release } = createReadModels(Employee);
|
|
@@ -169,7 +163,6 @@ describe('ReadModels', () => {
|
|
|
169
163
|
id = '';
|
|
170
164
|
}
|
|
171
165
|
fromEvent(SomeEvent)(Customer);
|
|
172
|
-
readModel('CustomerWithIdOnly')(Customer);
|
|
173
166
|
|
|
174
167
|
it('should fall back to the id property as the subject, unchanged from today', async () => {
|
|
175
168
|
const { readModels, release } = createReadModels(Customer);
|
|
@@ -187,7 +180,6 @@ describe('ReadModels', () => {
|
|
|
187
180
|
name = '';
|
|
188
181
|
}
|
|
189
182
|
fromEvent(SomeEvent)(Anonymous);
|
|
190
|
-
readModel('AnonymousReadModel')(Anonymous);
|
|
191
183
|
|
|
192
184
|
it('should throw, same as today', async () => {
|
|
193
185
|
const { readModels } = createReadModels(Anonymous);
|
|
@@ -202,7 +194,6 @@ describe('ReadModels', () => {
|
|
|
202
194
|
id = '';
|
|
203
195
|
}
|
|
204
196
|
fromEvent(SomeEvent)(Customer);
|
|
205
|
-
readModel('CustomerForReleaseMany')(Customer);
|
|
206
197
|
|
|
207
198
|
it('should release each instance using its own resolved subject', async () => {
|
|
208
199
|
const { readModels, release } = createReadModels(Customer);
|
package/readModels/ReadModels.ts
CHANGED
|
@@ -324,7 +324,7 @@ export class ReadModels implements IReadModels {
|
|
|
324
324
|
private resolveReadModel<TReadModel>(readModelType: Constructor<TReadModel>): ResolvedReadModel {
|
|
325
325
|
const [resolved] = this.resolveReadModels(readModelType);
|
|
326
326
|
if (!resolved) {
|
|
327
|
-
throw new Error(`Unknown read model '${readModelType.name}'. Make sure it is
|
|
327
|
+
throw new Error(`Unknown read model '${readModelType.name}'. Make sure it is discoverable through a projection, reducer, or model-bound mapping.`);
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
return resolved;
|
|
@@ -67,22 +67,41 @@ 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 deduplicate inferred types and retain
|
|
70
|
+
it('should deduplicate inferred types and retain custom identifiers', async () => {
|
|
71
71
|
const provider = DefaultClientArtifactsProvider.default;
|
|
72
|
-
for (const type of [ModelBound, Projected, Reduced
|
|
72
|
+
for (const type of [ModelBound, Projected, Reduced]) {
|
|
73
73
|
expect(provider.readModels.filter(model => model === type)).toHaveLength(1);
|
|
74
74
|
}
|
|
75
75
|
const { connection, registerMany } = createConnection();
|
|
76
76
|
await new ReadModels('store', 'Default', connection, provider, 'sink').register();
|
|
77
77
|
const definitions = registerMany.mock.calls[0][0].ReadModels;
|
|
78
78
|
const byId = new Map(definitions.map((definition: { Type: { Identifier: string } }) => [definition.Type.Identifier, definition]));
|
|
79
|
-
for (const id of ['ModelBound', 'projected-custom', 'Reduced'
|
|
79
|
+
for (const id of ['ModelBound', 'projected-custom', 'Reduced']) {
|
|
80
80
|
expect(byId.has(id)).toBe(true);
|
|
81
81
|
}
|
|
82
82
|
expect(JSON.parse(byId.get('projected-custom').Schema).properties.value.type).toBe('string');
|
|
83
83
|
expect(JSON.parse(byId.get('Reduced').Schema).properties.count.type).toBe('number');
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
it('should preserve the deprecated decorator identifier, schema, and discovery', async () => {
|
|
87
|
+
const provider = DefaultClientArtifactsProvider.default;
|
|
88
|
+
expect(provider.readModels).toContain(Legacy);
|
|
89
|
+
const { connection, registerMany } = createConnection();
|
|
90
|
+
await new ReadModels('store', 'Default', connection, provider, 'sink').register();
|
|
91
|
+
const definition = registerMany.mock.calls[0][0].ReadModels.find(
|
|
92
|
+
(model: { Type: { Identifier: string } }) => model.Type.Identifier === 'legacy-id');
|
|
93
|
+
expect(definition).toBeDefined();
|
|
94
|
+
expect(JSON.parse(definition.Schema).properties.value.type).toBe('string');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should explain how to discover an unknown model', async () => {
|
|
98
|
+
class Unknown {}
|
|
99
|
+
const provider = { ...DefaultClientArtifactsProvider.default, readModels: [], projections: [], reducers: [] };
|
|
100
|
+
const { connection } = createConnection();
|
|
101
|
+
await expect(new ReadModels('store', 'Default', connection, provider, 'sink').findInstanceById(Unknown, 'key'))
|
|
102
|
+
.rejects.toThrow('Make sure it is discoverable through a projection, reducer, or model-bound mapping.');
|
|
103
|
+
});
|
|
104
|
+
|
|
86
105
|
it('should reject two distinct models with the same identifier before registration', async () => {
|
|
87
106
|
class First { static readonly readModelId = 'collision'; }
|
|
88
107
|
class Second { static readonly readModelId = 'collision'; }
|
|
@@ -21,7 +21,7 @@ const artifacts = {
|
|
|
21
21
|
constraints: [], projections: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
|
|
22
22
|
} as IClientArtifactsProvider;
|
|
23
23
|
|
|
24
|
-
describe('
|
|
24
|
+
describe('inferred read models with standard decorators', () => {
|
|
25
25
|
it('rejects an untyped array before registering a reducer read model', async () => {
|
|
26
26
|
const registerMany = vi.fn();
|
|
27
27
|
const connection = { readModels: { registerMany } } as unknown as ChronicleConnection;
|
|
@@ -89,7 +89,7 @@ function createConnection() {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
describe('Reducers', () => {
|
|
92
|
-
describe('when a reducer produces
|
|
92
|
+
describe('when a reducer produces an inferred model', () => {
|
|
93
93
|
class NamedState {
|
|
94
94
|
static readonly readModelId = 'existing-state-id';
|
|
95
95
|
count = 0;
|
package/reducers/Reducers.ts
CHANGED
|
@@ -213,7 +213,7 @@ export class Reducers implements IReducers {
|
|
|
213
213
|
if (readModelMeta?.schema) {
|
|
214
214
|
return JSON.stringify(readModelMeta.schema);
|
|
215
215
|
}
|
|
216
|
-
//
|
|
216
|
+
// Generate the schema directly from the inferred model type.
|
|
217
217
|
return JSON.stringify(JsonSchemaGenerator.generate(metadata.readModel));
|
|
218
218
|
}
|
|
219
219
|
|
|
@@ -8,14 +8,13 @@ import { pii } from '../compliance/pii.js';
|
|
|
8
8
|
import { encrypted } from '../confidentiality/encrypted.js';
|
|
9
9
|
import { EncryptionScope } from '../confidentiality/EncryptionScope.js';
|
|
10
10
|
import { PIIAndEncryptedCombinedNotSupported } from '../confidentiality/PIIAndEncryptedCombinedNotSupported.js';
|
|
11
|
-
import {
|
|
11
|
+
import { JsonSchemaGenerator } from './JsonSchemaGenerator.js';
|
|
12
12
|
import { JsonSchema } from './JsonSchema.js';
|
|
13
13
|
|
|
14
|
-
// See JsonSchemaGenerator.spec.ts for why decorators are applied as plain function calls here
|
|
15
|
-
// rather than `@decorator` syntax, and why order matters.
|
|
14
|
+
// See JsonSchemaGenerator.spec.ts for why decorators are applied as plain function calls here.
|
|
16
15
|
|
|
17
16
|
function schemaFor(target: Function): JsonSchema {
|
|
18
|
-
return
|
|
17
|
+
return JsonSchemaGenerator.generate(target);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
describe('JsonSchemaGenerator - security metadata', () => {
|
|
@@ -26,7 +25,6 @@ describe('JsonSchemaGenerator - security metadata', () => {
|
|
|
26
25
|
}
|
|
27
26
|
encrypted()(PartnerCredentials);
|
|
28
27
|
|
|
29
|
-
readModel()(PartnerCredentials);
|
|
30
28
|
const schema = schemaFor(PartnerCredentials);
|
|
31
29
|
|
|
32
30
|
it('should mark every one of its own properties as EncryptedSubject', () => {
|
|
@@ -46,7 +44,6 @@ describe('JsonSchemaGenerator - security metadata', () => {
|
|
|
46
44
|
name = '';
|
|
47
45
|
}
|
|
48
46
|
encrypted(EncryptionScope.Subject, 'Partner API key')(Integration.prototype, 'apiKey');
|
|
49
|
-
readModel()(Integration);
|
|
50
47
|
const schema = schemaFor(Integration);
|
|
51
48
|
|
|
52
49
|
it('should mark the decorated property as EncryptedSubject', () => {
|
|
@@ -67,7 +64,6 @@ describe('JsonSchemaGenerator - security metadata', () => {
|
|
|
67
64
|
value = '';
|
|
68
65
|
}
|
|
69
66
|
encrypted(scope)(Secret.prototype, 'value');
|
|
70
|
-
readModel()(Secret);
|
|
71
67
|
const schema = schemaFor(Secret);
|
|
72
68
|
|
|
73
69
|
it(`should resolve to metadataType ${expectedMetadataType}`, () => {
|
|
@@ -88,7 +84,6 @@ describe('JsonSchemaGenerator - security metadata', () => {
|
|
|
88
84
|
class Configuration {
|
|
89
85
|
key: ApiKey = new ApiKey('');
|
|
90
86
|
}
|
|
91
|
-
readModel()(Configuration);
|
|
92
87
|
const schema = schemaFor(Configuration);
|
|
93
88
|
|
|
94
89
|
it('should mark the concept-typed property as EncryptedNamespace', () => {
|
|
@@ -105,7 +100,6 @@ describe('JsonSchemaGenerator - security metadata', () => {
|
|
|
105
100
|
contact: ContactDetails = new ContactDetails();
|
|
106
101
|
}
|
|
107
102
|
encrypted(EncryptionScope.Subject, 'Vendor contact information')(Vendor.prototype, 'contact');
|
|
108
|
-
readModel()(Vendor);
|
|
109
103
|
const schema = schemaFor(Vendor);
|
|
110
104
|
|
|
111
105
|
it('should push the security metadata down onto every leaf property', () => {
|
|
@@ -130,7 +124,6 @@ describe('JsonSchemaGenerator - security metadata', () => {
|
|
|
130
124
|
tokens: Token[] = [];
|
|
131
125
|
}
|
|
132
126
|
field(Array, { enumerable: true, genericArguments: [Token] })(Session.prototype, 'tokens');
|
|
133
|
-
readModel()(Session);
|
|
134
127
|
const schema = schemaFor(Session);
|
|
135
128
|
|
|
136
129
|
it('should carry the element concept security metadata onto the item schema', () => {
|
|
@@ -150,7 +143,7 @@ describe('JsonSchemaGenerator - security metadata', () => {
|
|
|
150
143
|
encrypted()(ConflictedValue.prototype, 'value');
|
|
151
144
|
|
|
152
145
|
it('should throw PIIAndEncryptedCombinedNotSupported when generating the schema', () => {
|
|
153
|
-
expect(() =>
|
|
146
|
+
expect(() => schemaFor(ConflictedValue)).toThrow(PIIAndEncryptedCombinedNotSupported);
|
|
154
147
|
});
|
|
155
148
|
});
|
|
156
149
|
|
|
@@ -162,7 +155,7 @@ describe('JsonSchemaGenerator - security metadata', () => {
|
|
|
162
155
|
encrypted()(MixedClass.prototype, 'value');
|
|
163
156
|
|
|
164
157
|
it('should throw PIIAndEncryptedCombinedNotSupported when generating the schema', () => {
|
|
165
|
-
expect(() =>
|
|
158
|
+
expect(() => schemaFor(MixedClass)).toThrow(PIIAndEncryptedCombinedNotSupported);
|
|
166
159
|
});
|
|
167
160
|
});
|
|
168
161
|
|
|
@@ -171,13 +164,11 @@ describe('JsonSchemaGenerator - security metadata', () => {
|
|
|
171
164
|
ssn = '';
|
|
172
165
|
}
|
|
173
166
|
pii()(OnlyPii.prototype, 'ssn');
|
|
174
|
-
readModel()(OnlyPii);
|
|
175
167
|
|
|
176
168
|
class OnlyEncrypted {
|
|
177
169
|
apiKey = '';
|
|
178
170
|
}
|
|
179
171
|
encrypted()(OnlyEncrypted.prototype, 'apiKey');
|
|
180
|
-
readModel()(OnlyEncrypted);
|
|
181
172
|
|
|
182
173
|
it('should not throw for either type', () => {
|
|
183
174
|
expect(() => schemaFor(OnlyPii)).not.toThrow();
|
|
@@ -6,19 +6,17 @@ import { ConceptAs, field, Guid } from '@cratis/fundamentals';
|
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
7
|
import { pii } from '../compliance/pii.js';
|
|
8
8
|
import { eventType, getEventTypeJsonSchemaFor } from '../events/eventTypeDecorator.js';
|
|
9
|
-
import {
|
|
9
|
+
import { JsonSchemaGenerator } from './JsonSchemaGenerator.js';
|
|
10
10
|
import { JsonSchema } from './JsonSchema.js';
|
|
11
11
|
|
|
12
12
|
// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
|
|
13
13
|
// fixtures don't depend on the test runner's decorator-syntax support - they exercise exactly
|
|
14
14
|
// the same decorator functions and metadata storage that `@decorator` syntax would invoke.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
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.
|
|
16
|
+
// Apply `pii()` before generating the schema so compliance metadata is available.
|
|
19
17
|
|
|
20
18
|
function schemaFor(target: Function): JsonSchema {
|
|
21
|
-
return
|
|
19
|
+
return JsonSchemaGenerator.generate(target);
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
describe('JsonSchemaGenerator', () => {
|
|
@@ -27,7 +25,6 @@ describe('JsonSchemaGenerator', () => {
|
|
|
27
25
|
class LegacyModel {
|
|
28
26
|
code = new LegacyCode('A');
|
|
29
27
|
}
|
|
30
|
-
readModel()(LegacyModel);
|
|
31
28
|
expect(schemaFor(LegacyModel).properties?.code.type).toBe('string');
|
|
32
29
|
});
|
|
33
30
|
|
|
@@ -53,7 +50,6 @@ describe('JsonSchemaGenerator', () => {
|
|
|
53
50
|
email = '';
|
|
54
51
|
}
|
|
55
52
|
pii()(PersonProfile);
|
|
56
|
-
readModel()(PersonProfile);
|
|
57
53
|
|
|
58
54
|
const schema = schemaFor(PersonProfile);
|
|
59
55
|
|
|
@@ -69,7 +65,6 @@ describe('JsonSchemaGenerator', () => {
|
|
|
69
65
|
ssn = '';
|
|
70
66
|
}
|
|
71
67
|
pii('Social security number')(Contact.prototype, 'ssn');
|
|
72
|
-
readModel()(Contact);
|
|
73
68
|
|
|
74
69
|
const schema = schemaFor(Contact);
|
|
75
70
|
|
|
@@ -96,7 +91,6 @@ describe('JsonSchemaGenerator', () => {
|
|
|
96
91
|
class Customer {
|
|
97
92
|
email: EmailAddress = new EmailAddress('');
|
|
98
93
|
}
|
|
99
|
-
readModel()(Customer);
|
|
100
94
|
|
|
101
95
|
const schema = schemaFor(Customer);
|
|
102
96
|
|
|
@@ -130,7 +124,6 @@ describe('JsonSchemaGenerator', () => {
|
|
|
130
124
|
}
|
|
131
125
|
pii()(Employee.prototype, 'name');
|
|
132
126
|
pii('Every field on this record is personal')(Employee);
|
|
133
|
-
readModel()(Employee);
|
|
134
127
|
|
|
135
128
|
const schema = schemaFor(Employee);
|
|
136
129
|
|
|
@@ -156,7 +149,6 @@ describe('JsonSchemaGenerator', () => {
|
|
|
156
149
|
contact: ContactDetails = new ContactDetails();
|
|
157
150
|
}
|
|
158
151
|
pii('Vendor contact information')(Vendor.prototype, 'contact');
|
|
159
|
-
readModel()(Vendor);
|
|
160
152
|
|
|
161
153
|
const schema = schemaFor(Vendor);
|
|
162
154
|
|
|
@@ -182,7 +174,6 @@ describe('JsonSchemaGenerator', () => {
|
|
|
182
174
|
codes: RequirementCode[] = [];
|
|
183
175
|
}
|
|
184
176
|
field(Array, { enumerable: true, genericArguments: [RequirementCode] })(Contract.prototype, 'codes');
|
|
185
|
-
readModel()(Contract);
|
|
186
177
|
|
|
187
178
|
const schema = schemaFor(Contract);
|
|
188
179
|
|
|
@@ -211,7 +202,6 @@ describe('JsonSchemaGenerator', () => {
|
|
|
211
202
|
items: PlainItem[] = [];
|
|
212
203
|
}
|
|
213
204
|
field(Array, { enumerable: true, genericArguments: [PlainItem] })(Basket.prototype, 'items');
|
|
214
|
-
readModel()(Basket);
|
|
215
205
|
|
|
216
206
|
const schema = schemaFor(Basket);
|
|
217
207
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { ConceptAs, field, Guid } from '@cratis/fundamentals';
|
|
5
5
|
import { describe, expect, it } from 'vitest';
|
|
6
6
|
import { eventType, getEventTypeMetadata } from '../events/eventTypeDecorator.js';
|
|
7
|
-
import {
|
|
7
|
+
import { JsonSchemaGenerator } from './JsonSchemaGenerator.js';
|
|
8
8
|
import { expectedProperties } from './decorators.expected.fixture.js';
|
|
9
9
|
import { getProjectionMetadata, projection } from '../projections/declarative/projection.js';
|
|
10
10
|
import { getReducerMetadata, reducer } from '../reducers/reducer.js';
|
|
@@ -35,7 +35,6 @@ class LegacyEvent {
|
|
|
35
35
|
@field(Quantity) quantity!: Quantity;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
@readModel('legacy-model')
|
|
39
38
|
class LegacyModel {
|
|
40
39
|
@field(Number) count!: number;
|
|
41
40
|
}
|
|
@@ -52,7 +51,7 @@ class LegacyReactor {}
|
|
|
52
51
|
describe('legacy decorator syntax', () => {
|
|
53
52
|
it('registers the same schemas as standard decorators', () => {
|
|
54
53
|
expect(getEventTypeMetadata(LegacyEvent)?.eventType.id.value).toBe('legacy-fixture');
|
|
55
|
-
expect(
|
|
54
|
+
expect(JsonSchemaGenerator.generate(LegacyModel).properties?.count.type).toBe('number');
|
|
56
55
|
expect(getEventTypeMetadata(LegacyEvent)?.schema.properties).toEqual(expectedProperties);
|
|
57
56
|
expect(getEventTypeMetadata(LegacyEvent)?.schema.required).toEqual(Object.keys(expectedProperties));
|
|
58
57
|
expect(getProjectionMetadata(LegacyProjection)?.id.value).toBe('legacy-projection');
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { ConceptAs, field, Guid, typeKey } from '@cratis/fundamentals';
|
|
5
5
|
import { describe, expect, it } from 'vitest';
|
|
6
6
|
import { eventType, getEventTypeMetadata } from '../events/eventTypeDecorator.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getReadModelId } from '../readModels/readModel.js';
|
|
8
8
|
import { getSetFromMetadata, setFrom } from '../projections/modelBound/setFrom.js';
|
|
9
9
|
import { getProjectionMetadata, projection } from '../projections/declarative/projection.js';
|
|
10
10
|
import { getReducerMetadata, reducer } from '../reducers/reducer.js';
|
|
@@ -39,8 +39,8 @@ class ReplacedEvent {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
@replaceClass
|
|
42
|
-
@readModel('replaced-model')
|
|
43
42
|
class ReplacedModel {
|
|
43
|
+
static readonly readModelId = 'replaced-model';
|
|
44
44
|
@field(Number) amount!: number;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -57,7 +57,6 @@ class StandardEvent {
|
|
|
57
57
|
@field(Quantity) quantity!: Quantity;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
@readModel('standard-model')
|
|
61
60
|
class StandardModel {
|
|
62
61
|
@field(Number)
|
|
63
62
|
@setFrom(StandardEvent)
|
|
@@ -87,7 +86,6 @@ class ChildMapping extends ParentMapping {
|
|
|
87
86
|
@eventType('empty-schema')
|
|
88
87
|
class MissingMembers {}
|
|
89
88
|
|
|
90
|
-
@readModel('empty-model')
|
|
91
89
|
class EmptyModel {}
|
|
92
90
|
|
|
93
91
|
@eventType('missing-field')
|
|
@@ -145,6 +143,9 @@ class UntypedArray {
|
|
|
145
143
|
@projection('standard-projection', StandardModel)
|
|
146
144
|
class StandardProjection {}
|
|
147
145
|
|
|
146
|
+
@projection('replaced-model-projection', ReplacedModel)
|
|
147
|
+
class ReplacedModelProjection {}
|
|
148
|
+
|
|
148
149
|
@reducer('standard-reducer', undefined, StandardModel)
|
|
149
150
|
class StandardReducer {}
|
|
150
151
|
|
|
@@ -157,20 +158,21 @@ const standardSchema = getEventTypeMetadata(StandardEvent)!.schema;
|
|
|
157
158
|
describe('standard decorator syntax', () => {
|
|
158
159
|
it('registers the event and read model with their declared member types', () => {
|
|
159
160
|
expect(getEventTypeMetadata(StandardEvent)?.eventType.id.value).toBe('standard-fixture');
|
|
160
|
-
expect(
|
|
161
|
+
expect(JsonSchemaGenerator.generate(StandardModel).properties?.count.type).toBe('number');
|
|
161
162
|
expect(getSetFromMetadata(StandardModel.prototype, 'count')).toEqual([{ eventType: StandardEvent, eventPropertyName: undefined }]);
|
|
162
163
|
expect(getSubjectPropertyName(StandardModel)).toBe('ownerId');
|
|
163
|
-
expect(
|
|
164
|
-
expect(
|
|
164
|
+
expect(JsonSchemaGenerator.generate(StandardModel).properties?.contact.compliance).toEqual([{ metadataType: 'PII', details: 'personal' }]);
|
|
165
|
+
expect(JsonSchemaGenerator.generate(StandardModel).properties?.secret.security).toEqual([{ metadataType: 'EncryptedSubject', details: '' }]);
|
|
165
166
|
expect(standardSchema.properties).toEqual(expectedProperties);
|
|
166
167
|
expect(standardSchema.required).toEqual(Object.keys(expectedProperties));
|
|
167
168
|
});
|
|
168
169
|
|
|
169
170
|
it('resolves schemas on an outer decorator replacement class', () => {
|
|
170
171
|
expect(getEventTypeMetadata(ReplacedEvent)!.schema.properties?.value.type).toBe('string');
|
|
171
|
-
expect(
|
|
172
|
+
expect(JsonSchemaGenerator.generate(ReplacedModel).properties?.amount.type).toBe('number');
|
|
173
|
+
expect(getReadModelId(ReplacedModel)).toBe('replaced-model');
|
|
172
174
|
expect(TypeDiscoverer.default.getTypeByDecoratorTypeAndName(DecoratorType.EventType, 'replaced-event')).toBe(ReplacedEvent);
|
|
173
|
-
expect(
|
|
175
|
+
expect(getProjectionMetadata(ReplacedModelProjection)?.readModelType).toBe(ReplacedModel);
|
|
174
176
|
});
|
|
175
177
|
|
|
176
178
|
it('keeps inherited property metadata copy-on-write', () => {
|
|
@@ -205,7 +207,7 @@ describe('standard decorator syntax', () => {
|
|
|
205
207
|
|
|
206
208
|
it('rejects missing member metadata instead of registering an empty schema', () => {
|
|
207
209
|
expect(getEventTypeMetadata(MissingMembers)!.schema.properties).toEqual({});
|
|
208
|
-
expect(
|
|
210
|
+
expect(JsonSchemaGenerator.generate(EmptyModel).properties).toEqual({});
|
|
209
211
|
expect(() => getEventTypeMetadata(MissingField)!.schema).toThrow(/Cannot determine the type of MissingField.value/);
|
|
210
212
|
});
|
|
211
213
|
|
package/types/TypeDiscoverer.ts
CHANGED
|
@@ -75,7 +75,7 @@ export class TypeDiscoverer {
|
|
|
75
75
|
if (decoratorType === DecoratorType.ReadModel) {
|
|
76
76
|
const previous = Array.from(typesForDecorator).find(([, registered]) => registered === type);
|
|
77
77
|
if (previous && previous[0] !== discoveredName) {
|
|
78
|
-
//
|
|
78
|
+
// An explicit legacy registration overrides the type-name alias registered by an observer.
|
|
79
79
|
if (previous[0] !== type.name) return;
|
|
80
80
|
typesForDecorator.delete(previous[0]);
|
|
81
81
|
}
|