@cratis/chronicle 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/ChronicleClient.ts +4 -3
  2. package/EventStore.ts +4 -3
  3. package/README.md +40 -28
  4. package/compliance/ComplianceContracts.ts +59 -0
  5. package/compliance/IPIIManager.ts +12 -0
  6. package/compliance/PIIManager.spec.ts +50 -0
  7. package/compliance/PIIManager.ts +9 -0
  8. package/compliance/PIINotSupportedOnEventSourceId.ts +21 -0
  9. package/compliance/index.ts +2 -0
  10. package/compliance/pii.spec.ts +37 -0
  11. package/compliance/pii.ts +16 -1
  12. package/compliance/subject.spec.ts +45 -0
  13. package/compliance/subject.ts +64 -0
  14. package/connection/callResults.spec.ts +90 -0
  15. package/connection/callResults.ts +112 -0
  16. package/dist/ChronicleClient.d.ts.map +1 -1
  17. package/dist/ChronicleClient.js +4 -3
  18. package/dist/ChronicleClient.js.map +1 -1
  19. package/dist/EventStore.d.ts.map +1 -1
  20. package/dist/EventStore.js +4 -3
  21. package/dist/EventStore.js.map +1 -1
  22. package/dist/compliance/ComplianceContracts.d.ts +31 -0
  23. package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
  24. package/dist/compliance/ComplianceContracts.js +52 -0
  25. package/dist/compliance/ComplianceContracts.js.map +1 -1
  26. package/dist/compliance/IPIIManager.d.ts +11 -0
  27. package/dist/compliance/IPIIManager.d.ts.map +1 -1
  28. package/dist/compliance/PIIManager.d.ts +2 -0
  29. package/dist/compliance/PIIManager.d.ts.map +1 -1
  30. package/dist/compliance/PIIManager.js +8 -0
  31. package/dist/compliance/PIIManager.js.map +1 -1
  32. package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts +11 -0
  33. package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts.map +1 -0
  34. package/dist/compliance/PIINotSupportedOnEventSourceId.js +19 -0
  35. package/dist/compliance/PIINotSupportedOnEventSourceId.js.map +1 -0
  36. package/dist/compliance/index.d.ts +2 -0
  37. package/dist/compliance/index.d.ts.map +1 -1
  38. package/dist/compliance/index.js +2 -0
  39. package/dist/compliance/index.js.map +1 -1
  40. package/dist/compliance/pii.d.ts.map +1 -1
  41. package/dist/compliance/pii.js +13 -1
  42. package/dist/compliance/pii.js.map +1 -1
  43. package/dist/compliance/subject.d.ts +39 -0
  44. package/dist/compliance/subject.d.ts.map +1 -0
  45. package/dist/compliance/subject.js +58 -0
  46. package/dist/compliance/subject.js.map +1 -0
  47. package/dist/connection/callResults.d.ts +69 -0
  48. package/dist/connection/callResults.d.ts.map +1 -0
  49. package/dist/connection/callResults.js +74 -0
  50. package/dist/connection/callResults.js.map +1 -0
  51. package/dist/eventSequences/AppendOptions.d.ts +8 -0
  52. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  53. package/dist/eventSequences/EventForEventSourceId.d.ts +7 -0
  54. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  55. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  56. package/dist/eventSequences/EventSequence.js +17 -6
  57. package/dist/eventSequences/EventSequence.js.map +1 -1
  58. package/dist/events/EventContext.d.ts +3 -0
  59. package/dist/events/EventContext.d.ts.map +1 -1
  60. package/dist/events/Tag.d.ts +10 -0
  61. package/dist/events/Tag.d.ts.map +1 -0
  62. package/dist/events/Tag.js +16 -0
  63. package/dist/events/Tag.js.map +1 -0
  64. package/dist/events/constraints/Constraints.js +3 -3
  65. package/dist/events/constraints/Constraints.js.map +1 -1
  66. package/dist/events/filterEventsByTagDecorator.d.ts +35 -0
  67. package/dist/events/filterEventsByTagDecorator.d.ts.map +1 -0
  68. package/dist/events/filterEventsByTagDecorator.js +48 -0
  69. package/dist/events/filterEventsByTagDecorator.js.map +1 -0
  70. package/dist/events/index.d.ts +4 -0
  71. package/dist/events/index.d.ts.map +1 -1
  72. package/dist/events/index.js +4 -0
  73. package/dist/events/index.js.map +1 -1
  74. package/dist/events/mergeTags.d.ts +12 -0
  75. package/dist/events/mergeTags.d.ts.map +1 -0
  76. package/dist/events/mergeTags.js +26 -0
  77. package/dist/events/mergeTags.js.map +1 -0
  78. package/dist/events/tagDecorator.d.ts +38 -0
  79. package/dist/events/tagDecorator.d.ts.map +1 -0
  80. package/dist/events/tagDecorator.js +54 -0
  81. package/dist/events/tagDecorator.js.map +1 -0
  82. package/dist/jobs/IJobs.d.ts +4 -4
  83. package/dist/jobs/IJobs.d.ts.map +1 -1
  84. package/dist/jobs/Jobs.d.ts +4 -4
  85. package/dist/jobs/Jobs.d.ts.map +1 -1
  86. package/dist/jobs/Jobs.js +12 -10
  87. package/dist/jobs/Jobs.js.map +1 -1
  88. package/dist/observation/ObserverRunningState.d.ts +9 -15
  89. package/dist/observation/ObserverRunningState.d.ts.map +1 -1
  90. package/dist/observation/ObserverRunningState.js +8 -14
  91. package/dist/observation/ObserverRunningState.js.map +1 -1
  92. package/dist/observation/index.d.ts +1 -0
  93. package/dist/observation/index.d.ts.map +1 -1
  94. package/dist/observation/index.js +1 -0
  95. package/dist/observation/index.js.map +1 -1
  96. package/dist/observation/toObserverRunningState.d.ts +9 -0
  97. package/dist/observation/toObserverRunningState.d.ts.map +1 -0
  98. package/dist/observation/toObserverRunningState.js +26 -0
  99. package/dist/observation/toObserverRunningState.js.map +1 -0
  100. package/dist/projections/IProjections.d.ts +71 -1
  101. package/dist/projections/IProjections.d.ts.map +1 -1
  102. package/dist/projections/ProjectionQueryResult.d.ts +8 -0
  103. package/dist/projections/ProjectionQueryResult.d.ts.map +1 -0
  104. package/dist/projections/ProjectionQueryResult.js +4 -0
  105. package/dist/projections/ProjectionQueryResult.js.map +1 -0
  106. package/dist/projections/ProjectionState.d.ts +18 -0
  107. package/dist/projections/ProjectionState.d.ts.map +1 -0
  108. package/dist/projections/ProjectionState.js +4 -0
  109. package/dist/projections/ProjectionState.js.map +1 -0
  110. package/dist/projections/Projections.d.ts +46 -1
  111. package/dist/projections/Projections.d.ts.map +1 -1
  112. package/dist/projections/Projections.js +153 -6
  113. package/dist/projections/Projections.js.map +1 -1
  114. package/dist/projections/UnableToQueryProjection.d.ts +12 -0
  115. package/dist/projections/UnableToQueryProjection.d.ts.map +1 -0
  116. package/dist/projections/UnableToQueryProjection.js +17 -0
  117. package/dist/projections/UnableToQueryProjection.js.map +1 -0
  118. package/dist/projections/index.d.ts +3 -0
  119. package/dist/projections/index.d.ts.map +1 -1
  120. package/dist/projections/index.js +1 -0
  121. package/dist/projections/index.js.map +1 -1
  122. package/dist/projections/modelBound/eventSequence.d.ts +22 -0
  123. package/dist/projections/modelBound/eventSequence.d.ts.map +1 -0
  124. package/dist/projections/modelBound/eventSequence.js +34 -0
  125. package/dist/projections/modelBound/eventSequence.js.map +1 -0
  126. package/dist/projections/modelBound/fromAll.d.ts +26 -0
  127. package/dist/projections/modelBound/fromAll.d.ts.map +1 -0
  128. package/dist/projections/modelBound/fromAll.js +31 -0
  129. package/dist/projections/modelBound/fromAll.js.map +1 -0
  130. package/dist/projections/modelBound/index.d.ts +4 -0
  131. package/dist/projections/modelBound/index.d.ts.map +1 -1
  132. package/dist/projections/modelBound/index.js +3 -0
  133. package/dist/projections/modelBound/index.js.map +1 -1
  134. package/dist/projections/modelBound/noAutoMap.d.ts +26 -0
  135. package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -0
  136. package/dist/projections/modelBound/noAutoMap.js +44 -0
  137. package/dist/projections/modelBound/noAutoMap.js.map +1 -0
  138. package/dist/reactors/Reactors.d.ts.map +1 -1
  139. package/dist/reactors/Reactors.js +7 -3
  140. package/dist/reactors/Reactors.js.map +1 -1
  141. package/dist/readModels/MaterializedReadModels.d.ts +0 -1
  142. package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
  143. package/dist/readModels/MaterializedReadModels.js +2 -8
  144. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  145. package/dist/readModels/ReadModelSubjectResolver.d.ts +22 -0
  146. package/dist/readModels/ReadModelSubjectResolver.d.ts.map +1 -0
  147. package/dist/readModels/ReadModelSubjectResolver.js +38 -0
  148. package/dist/readModels/ReadModelSubjectResolver.js.map +1 -0
  149. package/dist/readModels/ReadModels.d.ts.map +1 -1
  150. package/dist/readModels/ReadModels.js +7 -7
  151. package/dist/readModels/ReadModels.js.map +1 -1
  152. package/dist/readModels/index.d.ts +1 -0
  153. package/dist/readModels/index.d.ts.map +1 -1
  154. package/dist/readModels/index.js +1 -0
  155. package/dist/readModels/index.js.map +1 -1
  156. package/dist/reducers/Reducers.d.ts.map +1 -1
  157. package/dist/reducers/Reducers.js +21 -3
  158. package/dist/reducers/Reducers.js.map +1 -1
  159. package/dist/schemas/JsonSchema.d.ts +14 -0
  160. package/dist/schemas/JsonSchema.d.ts.map +1 -1
  161. package/dist/schemas/JsonSchemaGenerator.d.ts +61 -5
  162. package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
  163. package/dist/schemas/JsonSchemaGenerator.js +130 -26
  164. package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
  165. package/dist/schemas/index.d.ts +1 -1
  166. package/dist/schemas/index.d.ts.map +1 -1
  167. package/dist/seeding/EventSeeding.d.ts.map +1 -1
  168. package/dist/seeding/EventSeeding.js +3 -1
  169. package/dist/seeding/EventSeeding.js.map +1 -1
  170. package/dist/tsconfig.tsbuildinfo +1 -1
  171. package/eventSequences/AppendOptions.ts +9 -0
  172. package/eventSequences/EventForEventSourceId.ts +9 -0
  173. package/eventSequences/EventSequence.ts +20 -6
  174. package/events/EventContext.ts +4 -0
  175. package/events/Tag.ts +14 -0
  176. package/events/constraints/Constraints.ts +3 -3
  177. package/events/filterEventsByTagDecorator.ts +51 -0
  178. package/events/index.ts +4 -0
  179. package/events/mergeTags.spec.ts +50 -0
  180. package/events/mergeTags.ts +27 -0
  181. package/events/tagDecorator.spec.ts +115 -0
  182. package/events/tagDecorator.ts +59 -0
  183. package/jobs/IJobs.ts +4 -4
  184. package/jobs/Jobs.ts +16 -14
  185. package/observation/ObserverRunningState.ts +9 -18
  186. package/observation/index.ts +1 -0
  187. package/observation/toObserverRunningState.ts +27 -0
  188. package/package.json +25 -10
  189. package/projections/IProjections.ts +81 -1
  190. package/projections/ProjectionQueryResult.ts +10 -0
  191. package/projections/ProjectionState.ts +25 -0
  192. package/projections/Projections.childrenAndNested.spec.ts +1 -1
  193. package/projections/Projections.modelBoundCompleteness.spec.ts +224 -0
  194. package/projections/Projections.operationalSurface.spec.ts +257 -0
  195. package/projections/Projections.spec.ts +1 -1
  196. package/projections/Projections.ts +180 -5
  197. package/projections/UnableToQueryProjection.ts +17 -0
  198. package/projections/index.ts +3 -0
  199. package/projections/modelBound/eventSequence.ts +38 -0
  200. package/projections/modelBound/fromAll.ts +42 -0
  201. package/projections/modelBound/index.ts +4 -0
  202. package/projections/modelBound/noAutoMap.ts +47 -0
  203. package/reactors/Reactors.ts +7 -3
  204. package/readModels/MaterializedReadModels.spec.ts +98 -0
  205. package/readModels/MaterializedReadModels.ts +2 -8
  206. package/readModels/ReadModelSubjectResolver.spec.ts +84 -0
  207. package/readModels/ReadModelSubjectResolver.ts +44 -0
  208. package/readModels/ReadModels.spec.ts +118 -0
  209. package/readModels/ReadModels.ts +7 -7
  210. package/readModels/index.ts +1 -0
  211. package/reducers/Reducers.spec.ts +80 -0
  212. package/reducers/Reducers.ts +22 -3
  213. package/schemas/JsonSchema.ts +16 -0
  214. package/schemas/JsonSchemaGenerator.spec.ts +195 -0
  215. package/schemas/JsonSchemaGenerator.ts +145 -27
  216. package/schemas/index.ts +1 -1
  217. package/seeding/EventSeeding.ts +3 -1
@@ -6,6 +6,7 @@ import { diag } from '@opentelemetry/api';
6
6
  import { SpanStatusCode } from '@opentelemetry/api';
7
7
  import { ChronicleOptions } from './ChronicleOptions';
8
8
  import { ChronicleConnection } from './connection';
9
+ import { ensureCommandSuccess, ensureQuerySuccess, firstQueryResult } from './connection/callResults';
9
10
  import { ConnectionLifecycle } from './connection/ConnectionLifecycle';
10
11
  import { KernelKeepAlive } from './connection/KernelKeepAlive';
11
12
  import { EventStore } from './EventStore';
@@ -131,7 +132,7 @@ export class ChronicleClient implements IChronicleClient {
131
132
  this._logger.debug('Ensuring event store exists in kernel', {
132
133
  eventStore: storeName.value
133
134
  });
134
- await this._connection.eventStores.ensure({ Name: storeName.value });
135
+ ensureCommandSuccess('ensure event store', await this._connection.eventStores.ensureEventStore({ Name: storeName.value }));
135
136
 
136
137
  const created = new EventStore(storeName, namespaceName, this._connection, this._lifecycle, this.options.defaultSinkTypeId);
137
138
  this._stores.set(key, created);
@@ -167,9 +168,9 @@ export class ChronicleClient implements IChronicleClient {
167
168
  try {
168
169
  const response = await this.withReconnect('get_event_stores', async () => {
169
170
  await this.ensureConnected();
170
- return this._connection.eventStores.getEventStores({});
171
+ return firstQueryResult('get event stores', this._connection.eventStores.allEventStores({}));
171
172
  });
172
- const result = (response.items ?? []).map((name: string) => new EventStoreName(name));
173
+ const result = ensureQuerySuccess('get event stores', response).map((name: string) => new EventStoreName(name));
173
174
  this._logger.verbose('Retrieved event stores from kernel', {
174
175
  count: result.length
175
176
  });
package/EventStore.ts CHANGED
@@ -5,6 +5,7 @@ import { diag } from '@opentelemetry/api';
5
5
  import { SpanStatusCode } from '@opentelemetry/api';
6
6
  import { ChronicleConnection } from './connection';
7
7
  import { ConnectionLifecycle } from './connection/ConnectionLifecycle';
8
+ import { ensureQuerySuccess, firstQueryResult } from './connection/callResults';
8
9
  import { EventLog } from './eventSequences/EventLog';
9
10
  import { EventSequence } from './eventSequences/EventSequence';
10
11
  import { EventSequenceId } from './eventSequences/EventSequenceId';
@@ -89,7 +90,7 @@ export class EventStore implements IEventStore {
89
90
  const artifacts = DefaultClientArtifactsProvider.default;
90
91
  this.eventTypes = new EventTypes(name.value, _connection, artifacts);
91
92
  this.constraints = new Constraints(name.value, _connection, artifacts);
92
- this.projections = new Projections(name.value, _connection, artifacts, defaultSinkTypeId);
93
+ this.projections = new Projections(name.value, namespace.value, _connection, artifacts, defaultSinkTypeId);
93
94
  this.reactors = new Reactors(artifacts, _connection, name.value, namespace.value, lifecycle, this.eventLog);
94
95
  this.reducers = new Reducers(artifacts, _connection, name.value, namespace.value, lifecycle, defaultSinkTypeId);
95
96
  this.readModels = new ReadModels(name.value, namespace.value, _connection, artifacts, defaultSinkTypeId);
@@ -163,8 +164,8 @@ export class EventStore implements IEventStore {
163
164
  return ChronicleTracer.startActiveSpan('chronicle.event_store.get_namespaces', async span => {
164
165
  span.setAttribute('chronicle.event_store', this.name.value);
165
166
  try {
166
- const response = await this._connection.namespaces.getNamespaces({ EventStore: this.name.value });
167
- const result = (response.items ?? []).map((namespace: string) => new EventStoreNamespaceName(namespace));
167
+ const response = await firstQueryResult('get namespaces', this._connection.namespaces.allNamespaces({ EventStore: this.name.value }));
168
+ const result = ensureQuerySuccess('get namespaces', response).map((namespace: string) => new EventStoreNamespaceName(namespace));
168
169
  span.setStatus({ code: SpanStatusCode.OK });
169
170
  return result;
170
171
  } catch (error) {
package/README.md CHANGED
@@ -1,38 +1,42 @@
1
1
  # Chronicle TypeScript Client
2
2
 
3
- A TypeScript-idiomatic client for [Cratis Chronicle](https://github.com/Cratis/Chronicle) — the open source event-sourcing kernel.
3
+ **Event sourcing for TypeScript and Node.js — the idiomatic client for [Cratis Chronicle](https://github.com/Cratis/Chronicle).**
4
+
5
+ [![npm](https://img.shields.io/npm/v/@cratis/chronicle?label=npm&logo=npm)](https://www.npmjs.com/package/@cratis/chronicle)
6
+ [![Build](https://github.com/Cratis/Chronicle.TypeScript/actions/workflows/build.yml/badge.svg)](https://github.com/Cratis/Chronicle.TypeScript/actions/workflows/build.yml)
7
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Cratis/Chronicle.TypeScript/blob/main/LICENSE)
8
+ [![Discord](https://img.shields.io/discord/1182595891576717413?label=Discord&logo=discord&logoColor=white)](https://discord.gg/kt4AMpV8WV)
9
+
10
+ Chronicle is an event-sourcing database and processing runtime with a first-class .NET SDK and additional TypeScript, Kotlin/Java (JVM), and Elixir clients — with a Python client coming soon — plus pluggable storage-provider implementations including MongoDB (default), PostgreSQL, SQL Server, and SQLite. This package is the **TypeScript client**.
4
11
 
5
12
  ## Overview
6
13
 
7
14
  `@cratis/chronicle` provides a clean, type-safe TypeScript API for interacting with the Chronicle Kernel. It builds on top of [`@cratis/chronicle.contracts`](https://www.npmjs.com/package/@cratis/chronicle.contracts) (the gRPC contracts package) and exposes idiomatic TypeScript constructs including:
8
15
 
9
- - **Decorators** — `@eventType`, `@eventTypeMigration`, `@readModel`, `@reactor`, `@reducer`, `@constraint`, `@projection`, and model-bound decorators such as `@fromEvent`
16
+ - **Decorators** — `@eventType`, `@eventTypeMigration`, `@readModel`, `@reactor`, `@reducer`, `@seeder`, `@constraint`, `@projection`, and model-bound decorators such as `@fromEvent`
10
17
  - **Value objects** — `EventSequenceNumber`, `EventTypeId`, `EventStoreName`, etc.
11
18
  - **Fluent client** — `ChronicleClient` → `EventStore` → `EventLog` → `append()`
12
19
 
13
- ## Structure
20
+ Beyond appending and observing events, the client covers the full Chronicle surface:
14
21
 
15
- ```
16
- Source/ ← @cratis/chronicle TypeScript library
17
- Documentation/ ← User-facing documentation
18
- Samples/
19
- Console/ ← Plain Node.js console sample application
20
- ```
22
+ - **Transactions** — group appends into a unit of work with a single commit
23
+ - **Jobs** inspect and control long-running kernel jobs
24
+ - **Webhooks** — push events to HTTP endpoints
25
+ - **Compliance / PII** — classify event data and handle personally identifiable information
26
+ - **OpenTelemetry** built-in metrics and tracing instrumentation
21
27
 
22
- ## Prerequisite: Chronicle Running
28
+ ## Installation
23
29
 
24
- You need a Chronicle Kernel available before running samples or application code.
30
+ ```bash
31
+ npm install @cratis/chronicle reflect-metadata
32
+ ```
25
33
 
26
- The easiest local setup is the development Docker image:
34
+ You need a Chronicle Kernel available. The easiest local setup is the development Docker image:
27
35
 
28
36
  ```bash
29
37
  docker run -p 35000:35000 cratis/chronicle:latest-development
30
38
  ```
31
39
 
32
- ## Getting Started
33
-
34
- See [Documentation/getting-started.md](./Documentation/getting-started.md) for installation and usage instructions.
35
-
36
40
  ## Quick Example
37
41
 
38
42
  ```typescript
@@ -51,19 +55,27 @@ console.log(`Appended at sequence number ${result.sequenceNumber.value}`);
51
55
  client.dispose();
52
56
  ```
53
57
 
54
- ## Building
58
+ ## Documentation
55
59
 
56
- ```bash
57
- yarn install
58
- yarn workspace @cratis/chronicle compile
59
- ```
60
+ See the [getting started guide](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/getting-started.md) and the rest of the [documentation](https://github.com/Cratis/Chronicle.TypeScript/tree/main/Documentation) for installation and usage instructions, or visit [cratis.io](https://www.cratis.io/chronicle/).
60
61
 
61
- ## Running the Console Sample
62
+ ## The Cratis ecosystem
62
63
 
63
- ```bash
64
- yarn install
65
- yarn workspace @cratis/chronicle-test-console build
66
- yarn workspace @cratis/chronicle-test-console start
67
- ```
64
+ This package is part of [Cratis](https://www.cratis.io) — free, MIT-licensed tools for building event-sourced and CQRS applications.
65
+
66
+ - **[Chronicle](https://github.com/Cratis/Chronicle)** — event-sourcing database and runtime. Orleans-based kernel, pluggable storage (MongoDB default; PostgreSQL, SQL Server, SQLite, in-memory), language-agnostic gRPC contracts. [Docs](https://www.cratis.io/chronicle/)
67
+ - **Chronicle clients** — first-class [.NET SDK](https://github.com/Cratis/Chronicle), plus [TypeScript](https://github.com/Cratis/Chronicle.TypeScript), [Kotlin/Java](https://github.com/Cratis/Chronicle.Kotlin), and [Elixir](https://github.com/Cratis/Chronicle.Elixir); [Python](https://github.com/Cratis/Chronicle.Python) coming soon (pre-alpha). AI agents connect through the [Chronicle MCP server](https://github.com/Cratis/Chronicle.Mcp).
68
+ - **[Arc](https://github.com/Cratis/Arc)** — opinionated CQRS framework for ASP.NET Core with commands, queries, validation, authorization, and TypeScript proxy generation. Works without event sourcing. [Docs](https://www.cratis.io/arc/)
69
+ - **[Components](https://github.com/Cratis/Components)** — React components aligned with Arc patterns. [Docs](https://www.cratis.io/components/)
70
+ - **[CLI](https://github.com/Cratis/cli) + Workbench** — inspect and diagnose Chronicle from the terminal or the browser. [Docs](https://www.cratis.io/cli/)
71
+ - **[Samples](https://github.com/Cratis/Samples)** — runnable event sourcing and CQRS samples for the whole stack
72
+
73
+ Everything Cratis publishes today is MIT licensed and free to use.
74
+
75
+ ---
76
+
77
+ <div align="center">
78
+
79
+ *Part of the [Cratis](https://www.cratis.io) platform · Licensed under the [MIT license](https://github.com/Cratis/Chronicle.TypeScript/blob/main/LICENSE)*
68
80
 
69
- Set the `CHRONICLE_CONNECTION` environment variable to override the default connection string (`chronicle://localhost:35000`).
81
+ </div>
@@ -33,6 +33,16 @@ export interface DeleteEncryptionKeyRequest {
33
33
  Identifier: string;
34
34
  }
35
35
 
36
+ /**
37
+ * Request to authorize a new encryption key for a PII encryption key identifier whose key was
38
+ * previously erased, so a later lawful lifecycle can protect their data again.
39
+ */
40
+ export interface AllowNewEncryptionKeyRequest {
41
+ EventStore: string;
42
+ Namespace: string;
43
+ Identifier: string;
44
+ }
45
+
36
46
  /**
37
47
  * An empty protobuf message.
38
48
  */
@@ -49,6 +59,7 @@ export type DeepPartial<T> = T extends Builtin ? T : T extends globalThis.Array<
49
59
  export interface ComplianceClient<CallOptionsExt = {}> {
50
60
  release(request: DeepPartial<ReleaseRequest>, options?: CallOptions & CallOptionsExt): Promise<ReleaseResponse>;
51
61
  deleteEncryptionKey(request: DeepPartial<DeleteEncryptionKeyRequest>, options?: CallOptions & CallOptionsExt): Promise<Empty>;
62
+ allowNewEncryptionKey(request: DeepPartial<AllowNewEncryptionKeyRequest>, options?: CallOptions & CallOptionsExt): Promise<Empty>;
52
63
  }
53
64
 
54
65
  export const ReleaseRequest = {
@@ -181,6 +192,46 @@ export const DeleteEncryptionKeyRequest = {
181
192
  }
182
193
  };
183
194
 
195
+ export const AllowNewEncryptionKeyRequest = {
196
+ encode(message: AllowNewEncryptionKeyRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
197
+ if (message.EventStore !== '') {
198
+ writer.uint32(10).string(message.EventStore);
199
+ }
200
+ if (message.Namespace !== '') {
201
+ writer.uint32(18).string(message.Namespace);
202
+ }
203
+ if (message.Identifier !== '') {
204
+ writer.uint32(26).string(message.Identifier);
205
+ }
206
+ return writer;
207
+ },
208
+
209
+ decode(input: BinaryReader | Uint8Array, length?: number): AllowNewEncryptionKeyRequest {
210
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
211
+ const end = length === undefined ? reader.len : reader.pos + length;
212
+ const message: AllowNewEncryptionKeyRequest = { EventStore: '', Namespace: '', Identifier: '' };
213
+ while (reader.pos < end) {
214
+ const tag = reader.uint32();
215
+ switch (tag >>> 3) {
216
+ case 1: message.EventStore = reader.string(); continue;
217
+ case 2: message.Namespace = reader.string(); continue;
218
+ case 3: message.Identifier = reader.string(); continue;
219
+ }
220
+ if ((tag & 7) === 4 || tag === 0) break;
221
+ reader.skip(tag & 7);
222
+ }
223
+ return message;
224
+ },
225
+
226
+ fromPartial(object: DeepPartial<AllowNewEncryptionKeyRequest>): AllowNewEncryptionKeyRequest {
227
+ return {
228
+ EventStore: object.EventStore ?? '',
229
+ Namespace: object.Namespace ?? '',
230
+ Identifier: object.Identifier ?? ''
231
+ };
232
+ }
233
+ };
234
+
184
235
  export const Empty = {
185
236
  encode(_: Empty, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
186
237
  return writer;
@@ -224,6 +275,14 @@ export const ComplianceDefinition = {
224
275
  responseType: Empty,
225
276
  responseStream: false as const,
226
277
  options: {}
278
+ },
279
+ allowNewEncryptionKey: {
280
+ name: 'AllowNewEncryptionKey',
281
+ requestType: AllowNewEncryptionKeyRequest,
282
+ requestStream: false as const,
283
+ responseType: Empty,
284
+ responseStream: false as const,
285
+ options: {}
227
286
  }
228
287
  }
229
288
  } as const;
@@ -14,4 +14,16 @@ export interface IPIIManager {
14
14
  * this operation cannot be undone.
15
15
  */
16
16
  deleteEncryptionKey(identifier: string): Promise<void>;
17
+
18
+ /**
19
+ * Authorizes a new encryption key for a PII encryption key identifier whose key was previously
20
+ * erased, so a later lawful lifecycle can protect their data again.
21
+ * @param identifier - The identifier of the encryption key to authorize a new key for.
22
+ * @remarks
23
+ * Erasing an identifier removes the key that exists now; it does not ban the identifier
24
+ * forever. This creates no key - it lets the next PII value written for the identifier
25
+ * provision a fresh, independent one, which can decrypt nothing written before the erasure.
26
+ * The erased key itself never comes back, whatever else happens.
27
+ */
28
+ allowNewEncryptionKeyFor(identifier: string): Promise<void>;
17
29
  }
@@ -0,0 +1,50 @@
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 { describe, expect, it, vi } from 'vitest';
5
+ import { ChronicleConnection } from '../connection';
6
+ import { PIIManager } from './PIIManager';
7
+
8
+ function createPIIManager() {
9
+ const deleteEncryptionKeyMock = vi.fn().mockResolvedValue(undefined);
10
+ const allowNewEncryptionKeyMock = vi.fn().mockResolvedValue(undefined);
11
+ const connection = {
12
+ compliance: {
13
+ deleteEncryptionKey: deleteEncryptionKeyMock,
14
+ allowNewEncryptionKey: allowNewEncryptionKeyMock
15
+ }
16
+ } as unknown as ChronicleConnection;
17
+
18
+ const manager = new PIIManager('test-store', 'test-namespace', connection);
19
+ return { manager, deleteEncryptionKeyMock, allowNewEncryptionKeyMock };
20
+ }
21
+
22
+ describe('PIIManager', () => {
23
+ describe('when deleting an encryption key', () => {
24
+ it('should call the compliance service with the event store, namespace, and identifier', async () => {
25
+ const { manager, deleteEncryptionKeyMock } = createPIIManager();
26
+
27
+ await manager.deleteEncryptionKey('some-subject');
28
+
29
+ expect(deleteEncryptionKeyMock).toHaveBeenCalledWith({
30
+ EventStore: 'test-store',
31
+ Namespace: 'test-namespace',
32
+ Identifier: 'some-subject'
33
+ });
34
+ });
35
+ });
36
+
37
+ describe('when allowing a new encryption key', () => {
38
+ it('should call the compliance service with the event store, namespace, and identifier', async () => {
39
+ const { manager, allowNewEncryptionKeyMock } = createPIIManager();
40
+
41
+ await manager.allowNewEncryptionKeyFor('some-subject');
42
+
43
+ expect(allowNewEncryptionKeyMock).toHaveBeenCalledWith({
44
+ EventStore: 'test-store',
45
+ Namespace: 'test-namespace',
46
+ Identifier: 'some-subject'
47
+ });
48
+ });
49
+ });
50
+ });
@@ -28,4 +28,13 @@ export class PIIManager implements IPIIManager {
28
28
  Identifier: identifier
29
29
  });
30
30
  }
31
+
32
+ /** @inheritdoc */
33
+ async allowNewEncryptionKeyFor(identifier: string): Promise<void> {
34
+ await this._connection.compliance.allowNewEncryptionKey({
35
+ EventStore: this._eventStore,
36
+ Namespace: this._namespace,
37
+ Identifier: identifier
38
+ });
39
+ }
31
40
  }
@@ -0,0 +1,21 @@
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
+ /**
5
+ * Error thrown when the {@link pii} decorator is applied to the `eventSourceId` property.
6
+ */
7
+ export class PIINotSupportedOnEventSourceId extends Error {
8
+ /**
9
+ * Initializes a new instance of the {@link PIINotSupportedOnEventSourceId} class.
10
+ * @param typeName - The name of the type the decorator was applied to.
11
+ */
12
+ constructor(typeName: string) {
13
+ super(
14
+ `The @pii() decorator cannot be applied to 'eventSourceId' on '${typeName}' because it is the event ` +
15
+ 'source identifier. Event source identifiers cannot be encrypted as they are required for event ' +
16
+ 'correlation. If the identifier itself is sensitive, use a non-sensitive surrogate value as the event ' +
17
+ 'source identifier and store the sensitive value in a separate property marked with @pii().'
18
+ );
19
+ this.name = 'PIINotSupportedOnEventSourceId';
20
+ }
21
+ }
@@ -4,6 +4,8 @@
4
4
  export { ComplianceMetadataType } from './ComplianceMetadataType';
5
5
  export type { ComplianceMetadata } from './ComplianceMetadata';
6
6
  export { pii, getPIIMetadata, hasPIIMetadata, getTypePIIMetadata, isPII } from './pii';
7
+ export { subject, hasSubjectMetadata, getSubjectPropertyName } from './subject';
8
+ export { PIINotSupportedOnEventSourceId } from './PIINotSupportedOnEventSourceId';
7
9
  export { ComplianceMetadataResolver } from './ComplianceMetadataResolver';
8
10
  export type { IPIIManager } from './IPIIManager';
9
11
  export { PIIManager } from './PIIManager';
@@ -0,0 +1,37 @@
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 { describe, expect, it } from 'vitest';
6
+ import { pii } from './pii';
7
+ import { PIINotSupportedOnEventSourceId } from './PIINotSupportedOnEventSourceId';
8
+
9
+ // Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
10
+ // fixtures don't depend on the test runner's decorator-syntax support.
11
+
12
+ describe('pii', () => {
13
+ describe('when applied to the eventSourceId property', () => {
14
+ class SomeEvent {
15
+ eventSourceId = '';
16
+ }
17
+
18
+ it('should throw PIINotSupportedOnEventSourceId', () => {
19
+ expect(() => pii()(SomeEvent.prototype, 'eventSourceId')).toThrow(PIINotSupportedOnEventSourceId);
20
+ });
21
+
22
+ it('should describe why the property cannot be encrypted', () => {
23
+ expect(() => pii()(SomeEvent.prototype, 'eventSourceId')).toThrow(/event source identifier/);
24
+ });
25
+ });
26
+
27
+ describe('when applied to any other property', () => {
28
+ class SomeEvent {
29
+ eventSourceId = '';
30
+ name = '';
31
+ }
32
+
33
+ it('should not throw', () => {
34
+ expect(() => pii()(SomeEvent.prototype, 'name')).not.toThrow();
35
+ });
36
+ });
37
+ });
package/compliance/pii.ts CHANGED
@@ -4,8 +4,12 @@
4
4
  import 'reflect-metadata';
5
5
  import type { ComplianceMetadata } from './ComplianceMetadata';
6
6
  import { ComplianceMetadataType } from './ComplianceMetadataType';
7
+ import { PIINotSupportedOnEventSourceId } from './PIINotSupportedOnEventSourceId';
7
8
  import { TypeIntrospector } from '../types';
8
9
 
10
+ /** The property name this client uses everywhere for the event source identifier. */
11
+ const EVENT_SOURCE_ID_PROPERTY = 'eventSourceId';
12
+
9
13
  /** Metadata key for PII decorator on properties. */
10
14
  const PII_PROPERTY_METADATA_KEY = 'chronicle:compliance:pii:property';
11
15
 
@@ -66,7 +70,18 @@ export function pii(details?: string): PropertyDecorator & ClassDecorator {
66
70
  // Property decorator usage
67
71
  if (propertyKey !== undefined) {
68
72
  const key = propertyKey.toString();
69
- TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
73
+ const declaringType = (target as { constructor: Function }).constructor;
74
+
75
+ // Encrypting the event source identifier would make its own decryption key
76
+ // unfindable - the identifier is required, in the clear, to correlate events and
77
+ // look up the key that protects everything else. Mirrors C#'s
78
+ // PIINotSupportedOnEventSourceId guard, which throws for the same reason when
79
+ // [PII] is applied to an EventSourceId/EventSourceId<T> type.
80
+ if (key === EVENT_SOURCE_ID_PROPERTY) {
81
+ throw new PIINotSupportedOnEventSourceId(declaringType.name);
82
+ }
83
+
84
+ TypeIntrospector.trackProperty(declaringType, key);
70
85
  const metadata: ComplianceMetadata = {
71
86
  metadataType: ComplianceMetadataType.PII,
72
87
  details: details ?? ''
@@ -0,0 +1,45 @@
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 { describe, expect, it } from 'vitest';
6
+ import { getSubjectPropertyName, hasSubjectMetadata, subject } from './subject';
7
+
8
+ // Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
9
+ // fixtures don't depend on the test runner's decorator-syntax support.
10
+
11
+ describe('subject', () => {
12
+ describe('when applied to a property', () => {
13
+ class SomeReadModel {
14
+ personId = '';
15
+ name = '';
16
+ }
17
+ subject()(SomeReadModel.prototype, 'personId');
18
+
19
+ it('should mark the decorated property with subject metadata', () => {
20
+ expect(hasSubjectMetadata(SomeReadModel.prototype, 'personId')).toBe(true);
21
+ });
22
+
23
+ it('should leave other properties without subject metadata', () => {
24
+ expect(hasSubjectMetadata(SomeReadModel.prototype, 'name')).toBe(false);
25
+ });
26
+
27
+ it('should record the decorated property name on the declaring type', () => {
28
+ expect(getSubjectPropertyName(SomeReadModel)).toBe('personId');
29
+ });
30
+ });
31
+
32
+ describe('when no property is decorated', () => {
33
+ class PlainReadModel {
34
+ id = '';
35
+ }
36
+
37
+ it('should not record a subject property on the declaring type', () => {
38
+ expect(getSubjectPropertyName(PlainReadModel)).toBeUndefined();
39
+ });
40
+
41
+ it('should report no subject metadata for its properties', () => {
42
+ expect(hasSubjectMetadata(PlainReadModel.prototype, 'id')).toBe(false);
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,64 @@
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 { TypeIntrospector } from '../types';
6
+
7
+ /** Metadata key for the subject decorator on a property. */
8
+ const SUBJECT_PROPERTY_METADATA_KEY = 'chronicle:compliance:subject:property';
9
+
10
+ /** Metadata key for the name of the subject property recorded on the declaring type. */
11
+ const SUBJECT_TYPE_METADATA_KEY = 'chronicle:compliance:subject:type';
12
+
13
+ /**
14
+ * Decorator that marks a property as the compliance subject - the natural person whose Personal
15
+ * Identifiable Information (PII) a read model or event carries. The subject selects which
16
+ * encryption key protects that PII, and which key a manual release operation must use.
17
+ *
18
+ * Mirrors the .NET client's `SubjectAttribute`. When no property is decorated, resolvers fall
19
+ * back to the `id` property by convention, so read models that predate this decorator keep
20
+ * working unchanged.
21
+ *
22
+ * @returns A property decorator.
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * @readModel()
27
+ * class Employee {
28
+ * @subject()
29
+ * personId: string = '';
30
+ *
31
+ * @pii('Employee social security number')
32
+ * ssn: string = '';
33
+ * }
34
+ * ```
35
+ */
36
+ export function subject(): PropertyDecorator {
37
+ return (target: object, propertyKey: string | symbol) => {
38
+ const key = propertyKey.toString();
39
+ const declaringType = (target as { constructor: Function }).constructor;
40
+
41
+ TypeIntrospector.trackProperty(declaringType, key);
42
+ Reflect.defineMetadata(SUBJECT_PROPERTY_METADATA_KEY, true, target, key);
43
+ Reflect.defineMetadata(SUBJECT_TYPE_METADATA_KEY, key, declaringType);
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Checks whether a property has been decorated with @subject.
49
+ * @param target - The class prototype.
50
+ * @param propertyKey - The property name.
51
+ * @returns True if the property has the @subject decorator; false otherwise.
52
+ */
53
+ export function hasSubjectMetadata(target: object, propertyKey: string): boolean {
54
+ return Reflect.hasMetadata(SUBJECT_PROPERTY_METADATA_KEY, target, propertyKey);
55
+ }
56
+
57
+ /**
58
+ * Gets the name of the property decorated with @subject on a type, if any.
59
+ * @param type - The type constructor to inspect.
60
+ * @returns The decorated property name, or undefined when no property is decorated.
61
+ */
62
+ export function getSubjectPropertyName(type: Function): string | undefined {
63
+ return Reflect.getMetadata(SUBJECT_TYPE_METADATA_KEY, type);
64
+ }
@@ -0,0 +1,90 @@
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 { describe, expect, it } from 'vitest';
5
+ import { ChronicleCallFailed, ensureCommandSuccess, ensureQuerySuccess, firstQueryResult, isCallSuccess } from './callResults';
6
+
7
+ const successfulResult = {
8
+ ValidationResults: [],
9
+ ExceptionMessages: [],
10
+ AuthorizationFailureReason: ''
11
+ };
12
+
13
+ describe('callResults', () => {
14
+ describe('when checking a successful result', () => {
15
+ it('should report success', () => {
16
+ expect(isCallSuccess(successfulResult)).toBe(true);
17
+ });
18
+ });
19
+
20
+ describe('when checking a result with an authorization failure reason', () => {
21
+ it('should report failure', () => {
22
+ expect(isCallSuccess({ ...successfulResult, AuthorizationFailureReason: 'no access' })).toBe(false);
23
+ });
24
+ });
25
+
26
+ describe('when checking a result with validation results', () => {
27
+ it('should report failure', () => {
28
+ expect(isCallSuccess({ ...successfulResult, ValidationResults: [{ Message: 'invalid', Members: [] }] })).toBe(false);
29
+ });
30
+ });
31
+
32
+ describe('when checking a result with exception messages', () => {
33
+ it('should report failure', () => {
34
+ expect(isCallSuccess({ ...successfulResult, ExceptionMessages: ['boom'] })).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('when ensuring a successful command', () => {
39
+ it('should not throw', () => {
40
+ expect(() => ensureCommandSuccess('operation', successfulResult)).not.toThrow();
41
+ });
42
+ });
43
+
44
+ describe('when ensuring a failed command', () => {
45
+ it('should throw with the failure reasons', () => {
46
+ expect(() => ensureCommandSuccess('operation', { ...successfulResult, ExceptionMessages: ['boom'] }))
47
+ .toThrow(ChronicleCallFailed);
48
+ expect(() => ensureCommandSuccess('operation', { ...successfulResult, ExceptionMessages: ['boom'] }))
49
+ .toThrow(/operation.*boom/);
50
+ });
51
+ });
52
+
53
+ describe('when ensuring a successful query', () => {
54
+ it('should return the data', () => {
55
+ expect(ensureQuerySuccess('operation', { ...successfulResult, Data: ['a', 'b'] })).toEqual(['a', 'b']);
56
+ });
57
+ });
58
+
59
+ describe('when ensuring a failed query', () => {
60
+ it('should throw with the failure reasons', () => {
61
+ expect(() => ensureQuerySuccess('operation', { ...successfulResult, AuthorizationFailureReason: 'no access', Data: [] }))
62
+ .toThrow(/no access/);
63
+ });
64
+ });
65
+
66
+ describe('when taking the first result from a stream', () => {
67
+ it('should return the first item and cancel the stream', async () => {
68
+ let cleanedUp = false;
69
+ async function* stream() {
70
+ try {
71
+ yield 'first';
72
+ yield 'second';
73
+ } finally {
74
+ cleanedUp = true;
75
+ }
76
+ }
77
+
78
+ const result = await firstQueryResult('operation', stream());
79
+ expect(result).toBe('first');
80
+ expect(cleanedUp).toBe(true);
81
+ });
82
+ });
83
+
84
+ describe('when taking the first result from an empty stream', () => {
85
+ it('should throw', async () => {
86
+ async function* stream() { /* produces nothing */ }
87
+ await expect(firstQueryResult('operation', stream())).rejects.toThrow(/completed without producing a result/);
88
+ });
89
+ });
90
+ });