@cratis/chronicle 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ChronicleClient.ts +4 -3
- package/EventStore.ts +4 -3
- package/README.md +40 -28
- package/compliance/ComplianceContracts.ts +59 -0
- package/compliance/IPIIManager.ts +12 -0
- package/compliance/PIIManager.spec.ts +50 -0
- package/compliance/PIIManager.ts +9 -0
- package/compliance/PIINotSupportedOnEventSourceId.ts +21 -0
- package/compliance/index.ts +2 -0
- package/compliance/pii.spec.ts +37 -0
- package/compliance/pii.ts +16 -1
- package/compliance/subject.spec.ts +45 -0
- package/compliance/subject.ts +64 -0
- package/connection/callResults.spec.ts +90 -0
- package/connection/callResults.ts +112 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +4 -3
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +4 -3
- package/dist/EventStore.js.map +1 -1
- package/dist/compliance/ComplianceContracts.d.ts +31 -0
- package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
- package/dist/compliance/ComplianceContracts.js +52 -0
- package/dist/compliance/ComplianceContracts.js.map +1 -1
- package/dist/compliance/IPIIManager.d.ts +11 -0
- package/dist/compliance/IPIIManager.d.ts.map +1 -1
- package/dist/compliance/PIIManager.d.ts +2 -0
- package/dist/compliance/PIIManager.d.ts.map +1 -1
- package/dist/compliance/PIIManager.js +8 -0
- package/dist/compliance/PIIManager.js.map +1 -1
- package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts +11 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts.map +1 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.js +19 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.js.map +1 -0
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.d.ts.map +1 -1
- package/dist/compliance/index.js +2 -0
- package/dist/compliance/index.js.map +1 -1
- package/dist/compliance/pii.d.ts.map +1 -1
- package/dist/compliance/pii.js +13 -1
- package/dist/compliance/pii.js.map +1 -1
- package/dist/compliance/subject.d.ts +39 -0
- package/dist/compliance/subject.d.ts.map +1 -0
- package/dist/compliance/subject.js +58 -0
- package/dist/compliance/subject.js.map +1 -0
- package/dist/connection/callResults.d.ts +69 -0
- package/dist/connection/callResults.d.ts.map +1 -0
- package/dist/connection/callResults.js +74 -0
- package/dist/connection/callResults.js.map +1 -0
- package/dist/eventSequences/AppendOptions.d.ts +8 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +7 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +17 -6
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/events/EventContext.d.ts +3 -0
- package/dist/events/EventContext.d.ts.map +1 -1
- package/dist/events/Tag.d.ts +10 -0
- package/dist/events/Tag.d.ts.map +1 -0
- package/dist/events/Tag.js +16 -0
- package/dist/events/Tag.js.map +1 -0
- package/dist/events/constraints/Constraints.js +3 -3
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/events/filterEventsByTagDecorator.d.ts +35 -0
- package/dist/events/filterEventsByTagDecorator.d.ts.map +1 -0
- package/dist/events/filterEventsByTagDecorator.js +48 -0
- package/dist/events/filterEventsByTagDecorator.js.map +1 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +4 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/mergeTags.d.ts +12 -0
- package/dist/events/mergeTags.d.ts.map +1 -0
- package/dist/events/mergeTags.js +26 -0
- package/dist/events/mergeTags.js.map +1 -0
- package/dist/events/tagDecorator.d.ts +38 -0
- package/dist/events/tagDecorator.d.ts.map +1 -0
- package/dist/events/tagDecorator.js +54 -0
- package/dist/events/tagDecorator.js.map +1 -0
- package/dist/jobs/IJobs.d.ts +4 -4
- package/dist/jobs/IJobs.d.ts.map +1 -1
- package/dist/jobs/Jobs.d.ts +4 -4
- package/dist/jobs/Jobs.d.ts.map +1 -1
- package/dist/jobs/Jobs.js +12 -10
- package/dist/jobs/Jobs.js.map +1 -1
- package/dist/observation/ObserverRunningState.d.ts +9 -15
- package/dist/observation/ObserverRunningState.d.ts.map +1 -1
- package/dist/observation/ObserverRunningState.js +8 -14
- package/dist/observation/ObserverRunningState.js.map +1 -1
- package/dist/observation/index.d.ts +1 -0
- package/dist/observation/index.d.ts.map +1 -1
- package/dist/observation/index.js +1 -0
- package/dist/observation/index.js.map +1 -1
- package/dist/observation/toObserverRunningState.d.ts +9 -0
- package/dist/observation/toObserverRunningState.d.ts.map +1 -0
- package/dist/observation/toObserverRunningState.js +26 -0
- package/dist/observation/toObserverRunningState.js.map +1 -0
- package/dist/projections/IProjections.d.ts +71 -1
- package/dist/projections/IProjections.d.ts.map +1 -1
- package/dist/projections/ProjectionQueryResult.d.ts +8 -0
- package/dist/projections/ProjectionQueryResult.d.ts.map +1 -0
- package/dist/projections/ProjectionQueryResult.js +4 -0
- package/dist/projections/ProjectionQueryResult.js.map +1 -0
- package/dist/projections/ProjectionState.d.ts +18 -0
- package/dist/projections/ProjectionState.d.ts.map +1 -0
- package/dist/projections/ProjectionState.js +4 -0
- package/dist/projections/ProjectionState.js.map +1 -0
- package/dist/projections/Projections.d.ts +46 -1
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +153 -6
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/UnableToQueryProjection.d.ts +12 -0
- package/dist/projections/UnableToQueryProjection.d.ts.map +1 -0
- package/dist/projections/UnableToQueryProjection.js +17 -0
- package/dist/projections/UnableToQueryProjection.js.map +1 -0
- package/dist/projections/index.d.ts +3 -0
- package/dist/projections/index.d.ts.map +1 -1
- package/dist/projections/index.js +1 -0
- package/dist/projections/index.js.map +1 -1
- package/dist/projections/modelBound/eventSequence.d.ts +22 -0
- package/dist/projections/modelBound/eventSequence.d.ts.map +1 -0
- package/dist/projections/modelBound/eventSequence.js +34 -0
- package/dist/projections/modelBound/eventSequence.js.map +1 -0
- package/dist/projections/modelBound/fromAll.d.ts +26 -0
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -0
- package/dist/projections/modelBound/fromAll.js +31 -0
- package/dist/projections/modelBound/fromAll.js.map +1 -0
- package/dist/projections/modelBound/index.d.ts +4 -0
- package/dist/projections/modelBound/index.d.ts.map +1 -1
- package/dist/projections/modelBound/index.js +3 -0
- package/dist/projections/modelBound/index.js.map +1 -1
- package/dist/projections/modelBound/noAutoMap.d.ts +26 -0
- package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -0
- package/dist/projections/modelBound/noAutoMap.js +44 -0
- package/dist/projections/modelBound/noAutoMap.js.map +1 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +7 -3
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/readModels/MaterializedReadModels.d.ts +0 -1
- package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
- package/dist/readModels/MaterializedReadModels.js +2 -8
- package/dist/readModels/MaterializedReadModels.js.map +1 -1
- package/dist/readModels/ReadModelSubjectResolver.d.ts +22 -0
- package/dist/readModels/ReadModelSubjectResolver.d.ts.map +1 -0
- package/dist/readModels/ReadModelSubjectResolver.js +38 -0
- package/dist/readModels/ReadModelSubjectResolver.js.map +1 -0
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +7 -7
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/index.d.ts +1 -0
- package/dist/readModels/index.d.ts.map +1 -1
- package/dist/readModels/index.js +1 -0
- package/dist/readModels/index.js.map +1 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +21 -3
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/schemas/JsonSchema.d.ts +14 -0
- package/dist/schemas/JsonSchema.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts +61 -5
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +130 -26
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.js +3 -1
- package/dist/seeding/EventSeeding.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOptions.ts +9 -0
- package/eventSequences/EventForEventSourceId.ts +9 -0
- package/eventSequences/EventSequence.ts +20 -6
- package/events/EventContext.ts +4 -0
- package/events/Tag.ts +14 -0
- package/events/constraints/Constraints.ts +3 -3
- package/events/filterEventsByTagDecorator.ts +51 -0
- package/events/index.ts +4 -0
- package/events/mergeTags.spec.ts +50 -0
- package/events/mergeTags.ts +27 -0
- package/events/tagDecorator.spec.ts +115 -0
- package/events/tagDecorator.ts +59 -0
- package/jobs/IJobs.ts +4 -4
- package/jobs/Jobs.ts +16 -14
- package/observation/ObserverRunningState.ts +9 -18
- package/observation/index.ts +1 -0
- package/observation/toObserverRunningState.ts +27 -0
- package/package.json +25 -10
- package/projections/IProjections.ts +81 -1
- package/projections/ProjectionQueryResult.ts +10 -0
- package/projections/ProjectionState.ts +25 -0
- package/projections/Projections.childrenAndNested.spec.ts +1 -1
- package/projections/Projections.modelBoundCompleteness.spec.ts +224 -0
- package/projections/Projections.operationalSurface.spec.ts +257 -0
- package/projections/Projections.spec.ts +1 -1
- package/projections/Projections.ts +180 -5
- package/projections/UnableToQueryProjection.ts +17 -0
- package/projections/index.ts +3 -0
- package/projections/modelBound/eventSequence.ts +38 -0
- package/projections/modelBound/fromAll.ts +42 -0
- package/projections/modelBound/index.ts +4 -0
- package/projections/modelBound/noAutoMap.ts +47 -0
- package/reactors/Reactors.ts +7 -3
- package/readModels/MaterializedReadModels.spec.ts +98 -0
- package/readModels/MaterializedReadModels.ts +2 -8
- package/readModels/ReadModelSubjectResolver.spec.ts +84 -0
- package/readModels/ReadModelSubjectResolver.ts +44 -0
- package/readModels/ReadModels.spec.ts +118 -0
- package/readModels/ReadModels.ts +7 -7
- package/readModels/index.ts +1 -0
- package/reducers/Reducers.spec.ts +80 -0
- package/reducers/Reducers.ts +22 -3
- package/schemas/JsonSchema.ts +16 -0
- package/schemas/JsonSchemaGenerator.spec.ts +195 -0
- package/schemas/JsonSchemaGenerator.ts +145 -27
- package/schemas/index.ts +1 -1
- package/seeding/EventSeeding.ts +3 -1
package/ChronicleClient.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
171
|
+
return firstQueryResult('get event stores', this._connection.eventStores.allEventStores({}));
|
|
171
172
|
});
|
|
172
|
-
const result = (
|
|
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.
|
|
167
|
-
const result = (
|
|
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
|
-
|
|
3
|
+
**Event sourcing for TypeScript and Node.js — the idiomatic client for [Cratis Chronicle](https://github.com/Cratis/Chronicle).**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@cratis/chronicle)
|
|
6
|
+
[](https://github.com/Cratis/Chronicle.TypeScript/actions/workflows/build.yml)
|
|
7
|
+
[](https://github.com/Cratis/Chronicle.TypeScript/blob/main/LICENSE)
|
|
8
|
+
[](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
|
-
|
|
20
|
+
Beyond appending and observing events, the client covers the full Chronicle surface:
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
##
|
|
28
|
+
## Installation
|
|
23
29
|
|
|
24
|
-
|
|
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
|
-
##
|
|
58
|
+
## Documentation
|
|
55
59
|
|
|
56
|
-
|
|
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
|
-
##
|
|
62
|
+
## The Cratis ecosystem
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
+
});
|
package/compliance/PIIManager.ts
CHANGED
|
@@ -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
|
+
}
|
package/compliance/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
});
|