@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.
Files changed (85) hide show
  1. package/EventStore.ts +10 -4
  2. package/README.md +16 -5
  3. package/artifacts/validateArtifactSchemas.spec.ts +3 -3
  4. package/compliance/pii.ts +2 -2
  5. package/compliance/subject.ts +1 -1
  6. package/confidentiality/encrypted.ts +1 -1
  7. package/dist/EventStore.d.ts +1 -0
  8. package/dist/EventStore.d.ts.map +1 -1
  9. package/dist/EventStore.js +7 -4
  10. package/dist/EventStore.js.map +1 -1
  11. package/dist/compliance/pii.d.ts +2 -2
  12. package/dist/compliance/pii.js +2 -2
  13. package/dist/compliance/subject.d.ts +1 -1
  14. package/dist/compliance/subject.js +1 -1
  15. package/dist/confidentiality/encrypted.d.ts +1 -1
  16. package/dist/confidentiality/encrypted.js +1 -1
  17. package/dist/eventSequences/EventLog.d.ts +2 -1
  18. package/dist/eventSequences/EventLog.d.ts.map +1 -1
  19. package/dist/eventSequences/EventLog.js +2 -2
  20. package/dist/eventSequences/EventLog.js.map +1 -1
  21. package/dist/eventSequences/EventSequence.d.ts +3 -1
  22. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  23. package/dist/eventSequences/EventSequence.js +11 -6
  24. package/dist/eventSequences/EventSequence.js.map +1 -1
  25. package/dist/events/constraints/ConstraintBuilder.d.ts +2 -0
  26. package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
  27. package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
  28. package/dist/events/constraints/Constraints.d.ts +8 -3
  29. package/dist/events/constraints/Constraints.d.ts.map +1 -1
  30. package/dist/events/constraints/Constraints.js +147 -6
  31. package/dist/events/constraints/Constraints.js.map +1 -1
  32. package/dist/events/constraints/index.d.ts +2 -0
  33. package/dist/events/constraints/index.d.ts.map +1 -1
  34. package/dist/events/constraints/index.js +2 -0
  35. package/dist/events/constraints/index.js.map +1 -1
  36. package/dist/events/constraints/removeConstraint.d.ts +11 -0
  37. package/dist/events/constraints/removeConstraint.d.ts.map +1 -0
  38. package/dist/events/constraints/removeConstraint.js +24 -0
  39. package/dist/events/constraints/removeConstraint.js.map +1 -0
  40. package/dist/events/constraints/unique.d.ts +21 -0
  41. package/dist/events/constraints/unique.d.ts.map +1 -0
  42. package/dist/events/constraints/unique.js +37 -0
  43. package/dist/events/constraints/unique.js.map +1 -0
  44. package/dist/projections/Projections.js +1 -1
  45. package/dist/projections/Projections.js.map +1 -1
  46. package/dist/readModels/ReadModels.js +1 -1
  47. package/dist/readModels/ReadModels.js.map +1 -1
  48. package/dist/reducers/Reducers.js +1 -1
  49. package/dist/reducers/Reducers.js.map +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/dist/types/TypeDiscoverer.js +1 -1
  52. package/dist/types/TypeDiscoverer.js.map +1 -1
  53. package/eventSequences/EventLog.ts +4 -2
  54. package/eventSequences/EventSequence.ts +10 -6
  55. package/events/classDecorators.standard.spec.ts +45 -1
  56. package/events/constraints/ConstraintBuilder.ts +2 -0
  57. package/events/constraints/Constraints.ts +140 -7
  58. package/events/constraints/for_Constraints/when_combining_decorators_with_fluent_constraints.spec.ts +142 -0
  59. package/events/constraints/for_Constraints/when_inheriting_removal_decorators.legacy.spec.ts +36 -0
  60. package/events/constraints/for_Constraints/when_registering_decorated_constraints.spec.ts +63 -0
  61. package/events/constraints/for_Constraints/when_registering_decorated_event_types.spec.ts +90 -0
  62. package/events/constraints/for_Constraints/when_rejecting_incompatible_unique_merges.spec.ts +108 -0
  63. package/events/constraints/for_Constraints/when_resolving_violation_messages.spec.ts +92 -0
  64. package/events/constraints/index.ts +2 -0
  65. package/events/constraints/removeConstraint.ts +28 -0
  66. package/events/constraints/unique.ts +45 -0
  67. package/package.json +1 -1
  68. package/projections/Projections.childrenAndNested.spec.ts +0 -2
  69. package/projections/Projections.modelBoundCompleteness.spec.ts +0 -7
  70. package/projections/Projections.operationalSurface.spec.ts +0 -4
  71. package/projections/Projections.spec.ts +0 -2
  72. package/projections/Projections.ts +1 -1
  73. package/projections/Projections.variants.spec.ts +0 -5
  74. package/readModels/MaterializedReadModels.spec.ts +0 -5
  75. package/readModels/ReadModels.spec.ts +0 -9
  76. package/readModels/ReadModels.ts +1 -1
  77. package/readModels/discovery.spec.ts +22 -3
  78. package/readModels/standardFallbacks.standard.spec.ts +1 -1
  79. package/reducers/Reducers.spec.ts +1 -1
  80. package/reducers/Reducers.ts +1 -1
  81. package/schemas/JsonSchemaGenerator.confidentiality.spec.ts +5 -14
  82. package/schemas/JsonSchemaGenerator.spec.ts +3 -13
  83. package/schemas/decorators.legacy.spec.ts +2 -3
  84. package/schemas/decorators.standard.spec.ts +12 -10
  85. 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() ?? properties);
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);
@@ -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 decorated and discoverable.`);
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 decorated and custom identifiers', async () => {
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, Legacy]) {
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', 'legacy-id']) {
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('standard read models without @readModel', () => {
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 a model without @readModel', () => {
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;
@@ -213,7 +213,7 @@ export class Reducers implements IReducers {
213
213
  if (readModelMeta?.schema) {
214
214
  return JSON.stringify(readModelMeta.schema);
215
215
  }
216
- // No @readModel() decorator — generate from instance
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 { getReadModelMetadata, readModel } from '../readModels/readModel.js';
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 getReadModelMetadata(target)!.schema;
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(() => { readModel()(ConflictedValue); }).toThrow(PIIAndEncryptedCombinedNotSupported);
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(() => { readModel()(MixedClass); }).toThrow(PIIAndEncryptedCombinedNotSupported);
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 { getReadModelMetadata, readModel } from '../readModels/readModel.js';
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
- // 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.
16
+ // Apply `pii()` before generating the schema so compliance metadata is available.
19
17
 
20
18
  function schemaFor(target: Function): JsonSchema {
21
- return getReadModelMetadata(target)!.schema;
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 { getReadModelMetadata, readModel } from '../readModels/readModel.js';
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(getReadModelMetadata(LegacyModel)?.schema.properties?.count.type).toBe('number');
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 { getReadModelMetadata, readModel } from '../readModels/readModel.js';
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(getReadModelMetadata(StandardModel)?.schema.properties?.count.type).toBe('number');
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(getReadModelMetadata(StandardModel)?.schema.properties?.contact.compliance).toEqual([{ metadataType: 'PII', details: 'personal' }]);
164
- expect(getReadModelMetadata(StandardModel)?.schema.properties?.secret.security).toEqual([{ metadataType: 'EncryptedSubject', details: '' }]);
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(getReadModelMetadata(ReplacedModel)!.schema.properties?.amount.type).toBe('number');
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(TypeDiscoverer.default.getTypeByDecoratorTypeAndName(DecoratorType.ReadModel, 'replaced-model')).toBe(ReplacedModel);
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(getReadModelMetadata(EmptyModel)!.schema.properties).toEqual({});
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
 
@@ -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
- // A named @readModel overrides the type-name alias registered by an observer.
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
  }