@cratis/chronicle 6.4.0 → 6.5.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 (65) hide show
  1. package/ChronicleClient.ts +1 -1
  2. package/EventStore.ts +5 -3
  3. package/artifacts/validateArtifactSchemas.spec.ts +14 -0
  4. package/auditing/AmbientContext.spec.ts +57 -0
  5. package/auditing/CausationManager.ts +13 -15
  6. package/correlation/CorrelationIdManager.ts +5 -0
  7. package/dist/ChronicleClient.js +1 -1
  8. package/dist/ChronicleClient.js.map +1 -1
  9. package/dist/EventStore.d.ts +3 -1
  10. package/dist/EventStore.d.ts.map +1 -1
  11. package/dist/EventStore.js +5 -3
  12. package/dist/EventStore.js.map +1 -1
  13. package/dist/auditing/CausationManager.d.ts +3 -1
  14. package/dist/auditing/CausationManager.d.ts.map +1 -1
  15. package/dist/auditing/CausationManager.js +9 -15
  16. package/dist/auditing/CausationManager.js.map +1 -1
  17. package/dist/correlation/CorrelationIdManager.d.ts +2 -0
  18. package/dist/correlation/CorrelationIdManager.d.ts.map +1 -1
  19. package/dist/correlation/CorrelationIdManager.js +4 -0
  20. package/dist/correlation/CorrelationIdManager.js.map +1 -1
  21. package/dist/eventSequences/AppendOptions.d.ts +3 -4
  22. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  23. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  24. package/dist/eventSequences/EventSequence.js +19 -14
  25. package/dist/eventSequences/EventSequence.js.map +1 -1
  26. package/dist/eventSequences/EventSequenceNumber.d.ts +3 -1
  27. package/dist/eventSequences/EventSequenceNumber.d.ts.map +1 -1
  28. package/dist/eventSequences/EventSequenceNumber.js +3 -1
  29. package/dist/eventSequences/EventSequenceNumber.js.map +1 -1
  30. package/dist/events/index.d.ts +1 -0
  31. package/dist/events/index.d.ts.map +1 -1
  32. package/dist/events/index.js +1 -0
  33. package/dist/events/index.js.map +1 -1
  34. package/dist/events/isRegisteredEvent.d.ts +4 -0
  35. package/dist/events/isRegisteredEvent.d.ts.map +1 -0
  36. package/dist/events/isRegisteredEvent.js +9 -0
  37. package/dist/events/isRegisteredEvent.js.map +1 -0
  38. package/dist/identity/IdentityProvider.d.ts +2 -0
  39. package/dist/identity/IdentityProvider.d.ts.map +1 -1
  40. package/dist/identity/IdentityProvider.js +4 -0
  41. package/dist/identity/IdentityProvider.js.map +1 -1
  42. package/dist/readModels/IReadModels.d.ts +10 -1
  43. package/dist/readModels/IReadModels.d.ts.map +1 -1
  44. package/dist/readModels/MaterializedReadModels.js +2 -2
  45. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  46. package/dist/readModels/ReadModels.d.ts +6 -1
  47. package/dist/readModels/ReadModels.d.ts.map +1 -1
  48. package/dist/readModels/ReadModels.js +28 -2
  49. package/dist/readModels/ReadModels.js.map +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/eventSequences/AppendOptions.ts +3 -4
  52. package/eventSequences/EventSequence.spec.ts +59 -0
  53. package/eventSequences/EventSequence.ts +21 -15
  54. package/eventSequences/EventSequenceNumber.ts +4 -1
  55. package/eventSequences/for_EventSequence/when_appending/with_metadata.spec.ts +2 -1
  56. package/events/index.ts +1 -0
  57. package/events/isRegisteredEvent.spec.ts +26 -0
  58. package/events/isRegisteredEvent.ts +12 -0
  59. package/identity/IdentityProvider.ts +5 -0
  60. package/package.json +1 -1
  61. package/readModels/IReadModels.ts +11 -1
  62. package/readModels/MaterializedReadModels.spec.ts +17 -6
  63. package/readModels/MaterializedReadModels.ts +2 -2
  64. package/readModels/ReadModels.spec.ts +96 -3
  65. package/readModels/ReadModels.ts +32 -2
@@ -2,11 +2,13 @@
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 type { Constructor } from '@cratis/fundamentals';
5
+ import { field, type Constructor } from '@cratis/fundamentals';
6
6
  import { describe, expect, it, vi } from 'vitest';
7
7
  import type { IClientArtifactsProvider } from '../artifacts/index.js';
8
8
  import type { ChronicleConnection } from '../connection/index.js';
9
9
  import { subject } from '../compliance/subject.js';
10
+ import { pii } from '../compliance/pii.js';
11
+ import { reducer } from '../reducers/reducer.js';
10
12
  import { fromEvent } from '../projections/modelBound/fromEvent.js';
11
13
  import { readModel } from './readModel.js';
12
14
  import { ReadModels } from './ReadModels.js';
@@ -20,8 +22,10 @@ class SomeEvent {
20
22
 
21
23
  function createReadModels(readModelType: Constructor, releaseResponse: Record<string, unknown> = { HasError: false, Payload: '{}' }) {
22
24
  const release = vi.fn().mockResolvedValue(releaseResponse);
25
+ const getInstanceByKey = vi.fn().mockResolvedValue({ ReadModel: '' });
23
26
  const connection = {
24
- compliance: { release }
27
+ compliance: { release },
28
+ readModels: { getInstanceByKey }
25
29
  } as unknown as ChronicleConnection;
26
30
 
27
31
  const clientArtifacts = {
@@ -38,10 +42,99 @@ function createReadModels(readModelType: Constructor, releaseResponse: Record<st
38
42
  } as IClientArtifactsProvider;
39
43
 
40
44
  const readModels = new ReadModels('test-store', 'test-namespace', connection, clientArtifacts, 'default-sink');
41
- return { readModels, release };
45
+ return { readModels, release, getInstanceByKey };
42
46
  }
43
47
 
44
48
  describe('ReadModels', () => {
49
+ describe('when no read model instance exists for a key', () => {
50
+ class MissingModel { id = ''; }
51
+ fromEvent(SomeEvent)(MissingModel);
52
+ readModel('MissingModel')(MissingModel);
53
+
54
+ it('should retain the legacy prototype-only instance for getInstanceById', async () => {
55
+ const { readModels, release } = createReadModels(MissingModel);
56
+ const instance = await readModels.getInstanceById(MissingModel, 'missing');
57
+ expect(instance).toBeInstanceOf(MissingModel);
58
+ expect(Object.keys(instance)).toEqual([]);
59
+ expect(release).not.toHaveBeenCalled();
60
+ });
61
+
62
+ it('should return null from findInstanceById without releasing PII', async () => {
63
+ const { readModels, release, getInstanceByKey } = createReadModels(MissingModel);
64
+ const instance = await readModels.findInstanceById(MissingModel, 'missing', 'session-1');
65
+ expect(instance).toBeNull();
66
+ expect(getInstanceByKey).toHaveBeenCalledWith(expect.objectContaining({ ReadModelKey: 'missing', SessionId: 'session-1' }));
67
+ expect(release).not.toHaveBeenCalled();
68
+ });
69
+ });
70
+
71
+ describe('when a read model instance exists for a key', () => {
72
+ class ExistingModel { id = ''; }
73
+ field(String)(ExistingModel.prototype, 'id');
74
+ fromEvent(SomeEvent)(ExistingModel);
75
+ readModel('ExistingModel')(ExistingModel);
76
+
77
+ it('should deserialize the model', async () => {
78
+ const { readModels, getInstanceByKey } = createReadModels(ExistingModel);
79
+ getInstanceByKey.mockResolvedValue({ ReadModel: '{"id":"found"}' });
80
+ const instance = await readModels.findInstanceById(ExistingModel, 'found');
81
+ expect(instance).toBeInstanceOf(ExistingModel);
82
+ expect(instance?.id).toBe('found');
83
+ const legacyInstance = await readModels.getInstanceById(ExistingModel, 'found');
84
+ expect(legacyInstance).toBeInstanceOf(ExistingModel);
85
+ expect(legacyInstance.id).toBe('found');
86
+ });
87
+ });
88
+
89
+ describe('when reading a compliance-bearing reducer', () => {
90
+ class PrivateModel { id = ''; ssn = ''; }
91
+ field(String)(PrivateModel.prototype, 'id');
92
+ field(String)(PrivateModel.prototype, 'ssn');
93
+ pii()(PrivateModel.prototype, 'ssn');
94
+ readModel('PrivateInstanceModel')(PrivateModel);
95
+ class PrivateReducer {}
96
+ reducer('PrivateInstanceReducer', undefined, PrivateModel)(PrivateReducer);
97
+
98
+ it('should reject both read methods when compliance release fails', async () => {
99
+ const release = vi.fn().mockResolvedValue({ HasError: true, Error: 'denied' });
100
+ const getInstanceByKey = vi.fn().mockResolvedValue({ ReadModel: '{"id":"a","ssn":"ciphertext"}' });
101
+ const connection = {
102
+ readModels: { getInstanceByKey },
103
+ compliance: { release }
104
+ } as unknown as ChronicleConnection;
105
+ const provider = { reducers: [PrivateReducer], projections: [], readModels: [] } as unknown as IClientArtifactsProvider;
106
+ const readModels = new ReadModels('store', 'tenant', connection, provider, 'sink');
107
+
108
+ await expect(readModels.getInstanceById(PrivateModel, 'a')).rejects.toThrow('Failed to release PII: denied');
109
+ await expect(readModels.findInstanceById(PrivateModel, 'a')).rejects.toThrow('Failed to release PII: denied');
110
+ expect(release).toHaveBeenCalledTimes(2);
111
+ });
112
+ });
113
+
114
+ describe('when watching a compliance-bearing reducer', () => {
115
+ class PrivateModel { id = ''; ssn = ''; }
116
+ field(String)(PrivateModel.prototype, 'id');
117
+ field(String)(PrivateModel.prototype, 'ssn');
118
+ pii()(PrivateModel.prototype, 'ssn');
119
+ readModel('PrivateReducerModel')(PrivateModel);
120
+ class PrivateReducer {}
121
+ reducer('PrivateReducer', undefined, PrivateModel)(PrivateReducer);
122
+
123
+ it('should reject rather than expose an unreleased changeset', async () => {
124
+ const release = vi.fn().mockResolvedValue({ HasError: true, Error: 'denied' });
125
+ const connection = {
126
+ readModels: { watch: async function* () {
127
+ yield { Namespace: 'tenant', ModelKey: 'a', ReadModel: '{"id":"a","ssn":"ciphertext"}', Removed: false };
128
+ } },
129
+ compliance: { release }
130
+ } as unknown as ChronicleConnection;
131
+ const provider = { reducers: [PrivateReducer], projections: [], readModels: [] } as unknown as IClientArtifactsProvider;
132
+ const readModels = new ReadModels('store', 'tenant', connection, provider, 'sink');
133
+ const iterator = readModels.watch(PrivateModel)[Symbol.asyncIterator]();
134
+ await expect(iterator.next()).rejects.toThrow('Failed to release PII: denied');
135
+ });
136
+ });
137
+
45
138
  describe('when releasing a read model with a property decorated with @subject()', () => {
46
139
  class Employee {
47
140
  id = '';
@@ -72,7 +72,10 @@ export class ReadModels implements IReadModels {
72
72
  });
73
73
  }
74
74
 
75
- /** @inheritdoc */
75
+ /**
76
+ * @inheritdoc
77
+ * @deprecated Use {@link findInstanceById} to distinguish an absent instance from a stored one.
78
+ */
76
79
  async getInstanceById<TReadModel>(readModelType: Constructor<TReadModel>, key: string, sessionId?: string): Promise<TReadModel> {
77
80
  const readModel = this.resolveReadModel(readModelType);
78
81
  const response = await this._connection.readModels.getInstanceByKey({
@@ -93,6 +96,30 @@ export class ReadModels implements IReadModels {
93
96
  return instance;
94
97
  }
95
98
 
99
+ /** @inheritdoc */
100
+ async findInstanceById<TReadModel>(readModelType: Constructor<TReadModel>, key: string, sessionId?: string): Promise<TReadModel | null> {
101
+ const readModel = this.resolveReadModel(readModelType);
102
+ const response = await this._connection.readModels.getInstanceByKey({
103
+ EventStore: this._eventStore,
104
+ Namespace: this._namespace,
105
+ ReadModelIdentifier: readModel.identifier,
106
+ EventSequenceId: readModel.eventSequenceId,
107
+ ReadModelKey: key,
108
+ SessionId: sessionId ?? ''
109
+ });
110
+
111
+ if (!response.ReadModel) {
112
+ return null;
113
+ }
114
+ const instance = this.deserializeReadModel(readModelType, response.ReadModel);
115
+
116
+ if (readModel.observerType === ContractReadModelObserverType.Reducer && this.schemaHasComplianceMetadata(readModel.schema)) {
117
+ return this.release(readModelType, instance);
118
+ }
119
+
120
+ return instance;
121
+ }
122
+
96
123
  /** @inheritdoc */
97
124
  async getInstances<TReadModel>(readModelType: Constructor<TReadModel>, eventCount: bigint = unlimitedEventCount): Promise<TReadModel[]> {
98
125
  const readModel = this.resolveReadModel(readModelType);
@@ -149,10 +176,13 @@ export class ReadModels implements IReadModels {
149
176
  ReadModelIdentifier: readModel.identifier,
150
177
  EventSequenceId: readModel.eventSequenceId
151
178
  })) {
179
+ const instance = this.deserializeReadModel(readModelType, changeset.ReadModel);
180
+ const requiresRelease = !changeset.Removed && readModel.observerType === ContractReadModelObserverType.Reducer &&
181
+ this.schemaHasComplianceMetadata(readModel.schema);
152
182
  yield {
153
183
  namespace: changeset.Namespace,
154
184
  key: changeset.ModelKey,
155
- readModel: this.deserializeReadModel(readModelType, changeset.ReadModel),
185
+ readModel: requiresRelease ? await this.release(readModelType, instance) : instance,
156
186
  removed: changeset.Removed
157
187
  };
158
188
  }