From 1a098250990ea2b9cfb7d85d8210ebb5a8f566c0 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:12:11 +0200 Subject: [PATCH 01/39] Add TypeScript snippets for configuration docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../camel-casing/aspnetcore-basic.md | 3 ++ .../camel-casing/aspnetcore-with-options.md | 3 ++ .../camel-casing/direct-client.md | 3 ++ .../configuration/camel-casing/event.md | 3 ++ .../configuration/camel-casing/projection.md | 3 ++ .../configuration/camel-casing/read-model.md | 3 ++ .../configuration/camel-casing/worker-host.md | 3 ++ .../chronicle-options/connection-string.md | 7 ++++ .../chronicle-options/enable-validation.md | 3 ++ .../chronicle-options/sql-sink.md | 9 +++++ .../grpc-message-size/configure.md | 3 ++ .../grpc-message-size/null-values.md | 3 ++ .../configuration/index/register.md | 8 ++++ .../aspnetcore-builder.md | 3 ++ .../chronicle-builder.md | 3 ++ .../custom-artifacts-provider-usage.md | 9 +++++ .../custom-artifacts-provider.md | 38 +++++++++++++++++++ .../default-artifacts-provider.md | 10 +++++ .../structural-dependencies/direct-client.md | 3 ++ .../configuration/tls/client-options.md | 3 ++ .../tls/connection-string-skip-validation.md | 7 ++++ .../configuration/tls/validation-enabled.md | 7 ++++ 22 files changed, 137 insertions(+) create mode 100644 Documentation/client-snippets/configuration/camel-casing/aspnetcore-basic.md create mode 100644 Documentation/client-snippets/configuration/camel-casing/aspnetcore-with-options.md create mode 100644 Documentation/client-snippets/configuration/camel-casing/direct-client.md create mode 100644 Documentation/client-snippets/configuration/camel-casing/event.md create mode 100644 Documentation/client-snippets/configuration/camel-casing/projection.md create mode 100644 Documentation/client-snippets/configuration/camel-casing/read-model.md create mode 100644 Documentation/client-snippets/configuration/camel-casing/worker-host.md create mode 100644 Documentation/client-snippets/configuration/chronicle-options/connection-string.md create mode 100644 Documentation/client-snippets/configuration/chronicle-options/enable-validation.md create mode 100644 Documentation/client-snippets/configuration/chronicle-options/sql-sink.md create mode 100644 Documentation/client-snippets/configuration/grpc-message-size/configure.md create mode 100644 Documentation/client-snippets/configuration/grpc-message-size/null-values.md create mode 100644 Documentation/client-snippets/configuration/index/register.md create mode 100644 Documentation/client-snippets/configuration/structural-dependencies/aspnetcore-builder.md create mode 100644 Documentation/client-snippets/configuration/structural-dependencies/chronicle-builder.md create mode 100644 Documentation/client-snippets/configuration/structural-dependencies/custom-artifacts-provider-usage.md create mode 100644 Documentation/client-snippets/configuration/structural-dependencies/custom-artifacts-provider.md create mode 100644 Documentation/client-snippets/configuration/structural-dependencies/default-artifacts-provider.md create mode 100644 Documentation/client-snippets/configuration/structural-dependencies/direct-client.md create mode 100644 Documentation/client-snippets/configuration/tls/client-options.md create mode 100644 Documentation/client-snippets/configuration/tls/connection-string-skip-validation.md create mode 100644 Documentation/client-snippets/configuration/tls/validation-enabled.md diff --git a/Documentation/client-snippets/configuration/camel-casing/aspnetcore-basic.md b/Documentation/client-snippets/configuration/camel-casing/aspnetcore-basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/camel-casing/aspnetcore-basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/camel-casing/aspnetcore-with-options.md b/Documentation/client-snippets/configuration/camel-casing/aspnetcore-with-options.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/camel-casing/aspnetcore-with-options.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/camel-casing/direct-client.md b/Documentation/client-snippets/configuration/camel-casing/direct-client.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/camel-casing/direct-client.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/camel-casing/event.md b/Documentation/client-snippets/configuration/camel-casing/event.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/camel-casing/event.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/camel-casing/projection.md b/Documentation/client-snippets/configuration/camel-casing/projection.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/camel-casing/projection.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/camel-casing/read-model.md b/Documentation/client-snippets/configuration/camel-casing/read-model.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/camel-casing/read-model.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/camel-casing/worker-host.md b/Documentation/client-snippets/configuration/camel-casing/worker-host.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/camel-casing/worker-host.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/chronicle-options/connection-string.md b/Documentation/client-snippets/configuration/chronicle-options/connection-string.md new file mode 100644 index 0000000..c6d0420 --- /dev/null +++ b/Documentation/client-snippets/configuration/chronicle-options/connection-string.md @@ -0,0 +1,7 @@ +```typescript +import { ChronicleOptions } from '@cratis/chronicle'; + +function createChronicleOptionsConnectionString(): ChronicleOptions { + return ChronicleOptions.fromConnectionString('chronicle://myserver:35000'); +} +``` diff --git a/Documentation/client-snippets/configuration/chronicle-options/enable-validation.md b/Documentation/client-snippets/configuration/chronicle-options/enable-validation.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/chronicle-options/enable-validation.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/chronicle-options/sql-sink.md b/Documentation/client-snippets/configuration/chronicle-options/sql-sink.md new file mode 100644 index 0000000..f467d05 --- /dev/null +++ b/Documentation/client-snippets/configuration/chronicle-options/sql-sink.md @@ -0,0 +1,9 @@ +```typescript +import { ChronicleOptions, WellKnownSinks } from '@cratis/chronicle'; + +function createChronicleOptionsSqlSink(): ChronicleOptions { + return ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { + defaultSinkTypeId: WellKnownSinks.SQL + }); +} +``` diff --git a/Documentation/client-snippets/configuration/grpc-message-size/configure.md b/Documentation/client-snippets/configuration/grpc-message-size/configure.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/grpc-message-size/configure.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/grpc-message-size/null-values.md b/Documentation/client-snippets/configuration/grpc-message-size/null-values.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/grpc-message-size/null-values.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/index/register.md b/Documentation/client-snippets/configuration/index/register.md new file mode 100644 index 0000000..caf9ea5 --- /dev/null +++ b/Documentation/client-snippets/configuration/index/register.md @@ -0,0 +1,8 @@ +```typescript +import { ChronicleClient, ChronicleOptions } from '@cratis/chronicle'; + +async function runConfigurationIndexRegistrationExample(): Promise { + const client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000')); + const eventStore = await client.getEventStore('my-store'); +} +``` diff --git a/Documentation/client-snippets/configuration/structural-dependencies/aspnetcore-builder.md b/Documentation/client-snippets/configuration/structural-dependencies/aspnetcore-builder.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/structural-dependencies/aspnetcore-builder.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/structural-dependencies/chronicle-builder.md b/Documentation/client-snippets/configuration/structural-dependencies/chronicle-builder.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/structural-dependencies/chronicle-builder.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/structural-dependencies/custom-artifacts-provider-usage.md b/Documentation/client-snippets/configuration/structural-dependencies/custom-artifacts-provider-usage.md new file mode 100644 index 0000000..9184bf8 --- /dev/null +++ b/Documentation/client-snippets/configuration/structural-dependencies/custom-artifacts-provider-usage.md @@ -0,0 +1,9 @@ +```typescript +import { ChronicleOptions } from '@cratis/chronicle'; + +function createStructuralDependenciesCustomArtifactsProviderOptions(): ChronicleOptions { + return ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { + clientArtifactsProvider: new StructuralDepsMyArtifactsProvider() + }); +} +``` diff --git a/Documentation/client-snippets/configuration/structural-dependencies/custom-artifacts-provider.md b/Documentation/client-snippets/configuration/structural-dependencies/custom-artifacts-provider.md new file mode 100644 index 0000000..ceac4fb --- /dev/null +++ b/Documentation/client-snippets/configuration/structural-dependencies/custom-artifacts-provider.md @@ -0,0 +1,38 @@ +```typescript +import { Constructor } from '@cratis/fundamentals'; +import { eventType, IClientArtifactsProvider, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class StructuralDepsBookBorrowed { + constructor(readonly bookId: string) {} +} + +@eventType() +class StructuralDepsBookReturned { + constructor(readonly bookId: string) {} +} + +class StructuralDepsBorrowedBook { + bookId = ''; +} + +@projection() +class StructuralDepsBorrowedBooksProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder.from(StructuralDepsBookBorrowed, _ => _ + .set(m => m.bookId).to(e => e.bookId)); + } +} + +class StructuralDepsMyArtifactsProvider implements IClientArtifactsProvider { + readonly eventTypes: Constructor[] = [StructuralDepsBookBorrowed, StructuralDepsBookReturned]; + readonly readModels: Constructor[] = []; + readonly reactors: Constructor[] = []; + readonly reducers: Constructor[] = []; + readonly seeders: Constructor[] = []; + readonly constraints: Constructor[] = []; + readonly projections: Constructor[] = [StructuralDepsBorrowedBooksProjection]; + readonly webhooks: Constructor[] = []; + readonly eventTypeMigrations: Constructor[] = []; +} +``` diff --git a/Documentation/client-snippets/configuration/structural-dependencies/default-artifacts-provider.md b/Documentation/client-snippets/configuration/structural-dependencies/default-artifacts-provider.md new file mode 100644 index 0000000..3a89b5c --- /dev/null +++ b/Documentation/client-snippets/configuration/structural-dependencies/default-artifacts-provider.md @@ -0,0 +1,10 @@ +```typescript +import { DefaultClientArtifactsProvider, TypeDiscoverer } from '@cratis/chronicle'; + +// TypeScript discovers artifacts by scanning files matching glob patterns rather than +// scanning loaded assemblies - TypeDiscoverer.default is backed by ChronicleOptions' +// discoveryPatterns. +function createStructuralDependenciesDefaultArtifactsProvider(): DefaultClientArtifactsProvider { + return new DefaultClientArtifactsProvider(TypeDiscoverer.default); +} +``` diff --git a/Documentation/client-snippets/configuration/structural-dependencies/direct-client.md b/Documentation/client-snippets/configuration/structural-dependencies/direct-client.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/structural-dependencies/direct-client.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/tls/client-options.md b/Documentation/client-snippets/configuration/tls/client-options.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/configuration/tls/client-options.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/configuration/tls/connection-string-skip-validation.md b/Documentation/client-snippets/configuration/tls/connection-string-skip-validation.md new file mode 100644 index 0000000..03d35d0 --- /dev/null +++ b/Documentation/client-snippets/configuration/tls/connection-string-skip-validation.md @@ -0,0 +1,7 @@ +```typescript +import { ChronicleOptions } from '@cratis/chronicle'; + +function createTlsConnectionStringSkipValidation(): ChronicleOptions { + return ChronicleOptions.fromConnectionString('chronicle://localhost:35000?skipTlsValidation=true'); +} +``` diff --git a/Documentation/client-snippets/configuration/tls/validation-enabled.md b/Documentation/client-snippets/configuration/tls/validation-enabled.md new file mode 100644 index 0000000..a0427b0 --- /dev/null +++ b/Documentation/client-snippets/configuration/tls/validation-enabled.md @@ -0,0 +1,7 @@ +```typescript +import { ChronicleOptions } from '@cratis/chronicle'; + +function createConfigurationTlsValidationEnabled(): ChronicleOptions { + return ChronicleOptions.fromConnectionString('chronicle://my-server:35000?skipTlsValidation=false'); +} +``` From db56cdac22b6063306d55f05be34be9619d2fa77 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:12:11 +0200 Subject: [PATCH 02/39] Add TypeScript snippets for connection-strings docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../connection-strings/configuration/register.md | 3 +++ .../dotnet-client/development-defaults-equivalent.md | 9 +++++++++ .../dotnet-client/development-defaults.md | 9 +++++++++ .../connection-strings/dotnet-client/fluent-builder.md | 3 +++ .../dotnet-client/from-connection-string.md | 7 +++++++ .../dotnet-client/redacting-for-logs.md | 3 +++ 6 files changed, 34 insertions(+) create mode 100644 Documentation/client-snippets/connection-strings/configuration/register.md create mode 100644 Documentation/client-snippets/connection-strings/dotnet-client/development-defaults-equivalent.md create mode 100644 Documentation/client-snippets/connection-strings/dotnet-client/development-defaults.md create mode 100644 Documentation/client-snippets/connection-strings/dotnet-client/fluent-builder.md create mode 100644 Documentation/client-snippets/connection-strings/dotnet-client/from-connection-string.md create mode 100644 Documentation/client-snippets/connection-strings/dotnet-client/redacting-for-logs.md diff --git a/Documentation/client-snippets/connection-strings/configuration/register.md b/Documentation/client-snippets/connection-strings/configuration/register.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/connection-strings/configuration/register.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/connection-strings/dotnet-client/development-defaults-equivalent.md b/Documentation/client-snippets/connection-strings/dotnet-client/development-defaults-equivalent.md new file mode 100644 index 0000000..5be60eb --- /dev/null +++ b/Documentation/client-snippets/connection-strings/dotnet-client/development-defaults-equivalent.md @@ -0,0 +1,9 @@ +```typescript +import { ChronicleOptions } from '@cratis/chronicle'; + +function createConnectionStringsDevelopmentDefaultsEquivalent(): ChronicleOptions { + return ChronicleOptions.fromConnectionString( + 'chronicle://chronicle-dev-client:chronicle-dev-secret@localhost:35000' + ); +} +``` diff --git a/Documentation/client-snippets/connection-strings/dotnet-client/development-defaults.md b/Documentation/client-snippets/connection-strings/dotnet-client/development-defaults.md new file mode 100644 index 0000000..a7a0227 --- /dev/null +++ b/Documentation/client-snippets/connection-strings/dotnet-client/development-defaults.md @@ -0,0 +1,9 @@ +```typescript +import { ChronicleOptions } from '@cratis/chronicle'; + +// ChronicleOptions.development() points at the local dev kernel on chronicle://localhost:35000 +// using the built-in development client credentials. +function createConnectionStringsDevelopmentDefaults(): ChronicleOptions { + return ChronicleOptions.development(); +} +``` diff --git a/Documentation/client-snippets/connection-strings/dotnet-client/fluent-builder.md b/Documentation/client-snippets/connection-strings/dotnet-client/fluent-builder.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/connection-strings/dotnet-client/fluent-builder.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/connection-strings/dotnet-client/from-connection-string.md b/Documentation/client-snippets/connection-strings/dotnet-client/from-connection-string.md new file mode 100644 index 0000000..c11c435 --- /dev/null +++ b/Documentation/client-snippets/connection-strings/dotnet-client/from-connection-string.md @@ -0,0 +1,7 @@ +```typescript +import { ChronicleOptions } from '@cratis/chronicle'; + +function createConnectionStringsFromConnectionString(): ChronicleOptions { + return ChronicleOptions.fromConnectionString('chronicle://myserver:35000'); +} +``` diff --git a/Documentation/client-snippets/connection-strings/dotnet-client/redacting-for-logs.md b/Documentation/client-snippets/connection-strings/dotnet-client/redacting-for-logs.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/connection-strings/dotnet-client/redacting-for-logs.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From d6d318b984d0d707ff3b676f2132993149869b44 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:12:11 +0200 Subject: [PATCH 03/39] Add TypeScript snippets for hosting docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- Documentation/client-snippets/hosting/aspire/certificates.md | 3 +++ .../client-snippets/hosting/aspire/complete-example-dev.md | 3 +++ .../client-snippets/hosting/aspire/complete-example.md | 3 +++ .../client-snippets/hosting/aspire/connecting-client.md | 3 +++ Documentation/client-snippets/hosting/aspire/dev-mode.md | 3 +++ .../client-snippets/hosting/aspire/mongo-replica-set-helper.md | 3 +++ .../client-snippets/hosting/aspire/mongo-replica-set.md | 3 +++ Documentation/client-snippets/hosting/aspire/mongo.md | 3 +++ Documentation/client-snippets/hosting/aspire/pin-image.md | 3 +++ Documentation/client-snippets/hosting/aspire/postgres.md | 3 +++ .../hosting/aspire/rotation-previous-certificate.md | 3 +++ Documentation/client-snippets/hosting/aspire/sqlite.md | 3 +++ Documentation/client-snippets/hosting/aspire/sqlserver.md | 3 +++ .../client-snippets/hosting/configuration/storage/in-memory.md | 3 +++ .../hosting/local-certificates/client-configuration.md | 3 +++ 15 files changed, 45 insertions(+) create mode 100644 Documentation/client-snippets/hosting/aspire/certificates.md create mode 100644 Documentation/client-snippets/hosting/aspire/complete-example-dev.md create mode 100644 Documentation/client-snippets/hosting/aspire/complete-example.md create mode 100644 Documentation/client-snippets/hosting/aspire/connecting-client.md create mode 100644 Documentation/client-snippets/hosting/aspire/dev-mode.md create mode 100644 Documentation/client-snippets/hosting/aspire/mongo-replica-set-helper.md create mode 100644 Documentation/client-snippets/hosting/aspire/mongo-replica-set.md create mode 100644 Documentation/client-snippets/hosting/aspire/mongo.md create mode 100644 Documentation/client-snippets/hosting/aspire/pin-image.md create mode 100644 Documentation/client-snippets/hosting/aspire/postgres.md create mode 100644 Documentation/client-snippets/hosting/aspire/rotation-previous-certificate.md create mode 100644 Documentation/client-snippets/hosting/aspire/sqlite.md create mode 100644 Documentation/client-snippets/hosting/aspire/sqlserver.md create mode 100644 Documentation/client-snippets/hosting/configuration/storage/in-memory.md create mode 100644 Documentation/client-snippets/hosting/local-certificates/client-configuration.md diff --git a/Documentation/client-snippets/hosting/aspire/certificates.md b/Documentation/client-snippets/hosting/aspire/certificates.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/certificates.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/complete-example-dev.md b/Documentation/client-snippets/hosting/aspire/complete-example-dev.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/complete-example-dev.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/complete-example.md b/Documentation/client-snippets/hosting/aspire/complete-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/complete-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/connecting-client.md b/Documentation/client-snippets/hosting/aspire/connecting-client.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/connecting-client.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/dev-mode.md b/Documentation/client-snippets/hosting/aspire/dev-mode.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/dev-mode.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/mongo-replica-set-helper.md b/Documentation/client-snippets/hosting/aspire/mongo-replica-set-helper.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/mongo-replica-set-helper.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/mongo-replica-set.md b/Documentation/client-snippets/hosting/aspire/mongo-replica-set.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/mongo-replica-set.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/mongo.md b/Documentation/client-snippets/hosting/aspire/mongo.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/mongo.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/pin-image.md b/Documentation/client-snippets/hosting/aspire/pin-image.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/pin-image.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/postgres.md b/Documentation/client-snippets/hosting/aspire/postgres.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/postgres.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/rotation-previous-certificate.md b/Documentation/client-snippets/hosting/aspire/rotation-previous-certificate.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/rotation-previous-certificate.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/sqlite.md b/Documentation/client-snippets/hosting/aspire/sqlite.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/sqlite.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/aspire/sqlserver.md b/Documentation/client-snippets/hosting/aspire/sqlserver.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/aspire/sqlserver.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/configuration/storage/in-memory.md b/Documentation/client-snippets/hosting/configuration/storage/in-memory.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/configuration/storage/in-memory.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/hosting/local-certificates/client-configuration.md b/Documentation/client-snippets/hosting/local-certificates/client-configuration.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/hosting/local-certificates/client-configuration.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 0fabfa1b58fb212920bc6f9140af7699f0b9caef Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:12:11 +0200 Subject: [PATCH 04/39] Add TypeScript snippets for namespaces docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../namespaces/aspnetcore/builder-custom-resolver.md | 3 +++ .../namespaces/aspnetcore/http-header-resolver.md | 3 +++ .../namespaces/aspnetcore/subdomain-resolver.md | 3 +++ .../namespaces/aspnetcore/type-based-custom-resolver.md | 3 +++ .../client-snippets/namespaces/aspnetcore/web-app-example.md | 3 +++ .../client-snippets/namespaces/dotnet-client/claims-based.md | 3 +++ .../namespaces/dotnet-client/default-resolver.md | 3 +++ .../namespaces/dotnet-client/hosted-app-configuration.md | 3 +++ .../client-snippets/namespaces/dotnet-client/resolver-shape.md | 3 +++ .../namespaces/dotnet-client/tenant-resolver-usage.md | 3 +++ .../namespaces/dotnet-client/tenant-resolver.md | 3 +++ 11 files changed, 33 insertions(+) create mode 100644 Documentation/client-snippets/namespaces/aspnetcore/builder-custom-resolver.md create mode 100644 Documentation/client-snippets/namespaces/aspnetcore/http-header-resolver.md create mode 100644 Documentation/client-snippets/namespaces/aspnetcore/subdomain-resolver.md create mode 100644 Documentation/client-snippets/namespaces/aspnetcore/type-based-custom-resolver.md create mode 100644 Documentation/client-snippets/namespaces/aspnetcore/web-app-example.md create mode 100644 Documentation/client-snippets/namespaces/dotnet-client/claims-based.md create mode 100644 Documentation/client-snippets/namespaces/dotnet-client/default-resolver.md create mode 100644 Documentation/client-snippets/namespaces/dotnet-client/hosted-app-configuration.md create mode 100644 Documentation/client-snippets/namespaces/dotnet-client/resolver-shape.md create mode 100644 Documentation/client-snippets/namespaces/dotnet-client/tenant-resolver-usage.md create mode 100644 Documentation/client-snippets/namespaces/dotnet-client/tenant-resolver.md diff --git a/Documentation/client-snippets/namespaces/aspnetcore/builder-custom-resolver.md b/Documentation/client-snippets/namespaces/aspnetcore/builder-custom-resolver.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/aspnetcore/builder-custom-resolver.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/namespaces/aspnetcore/http-header-resolver.md b/Documentation/client-snippets/namespaces/aspnetcore/http-header-resolver.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/aspnetcore/http-header-resolver.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/namespaces/aspnetcore/subdomain-resolver.md b/Documentation/client-snippets/namespaces/aspnetcore/subdomain-resolver.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/aspnetcore/subdomain-resolver.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/namespaces/aspnetcore/type-based-custom-resolver.md b/Documentation/client-snippets/namespaces/aspnetcore/type-based-custom-resolver.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/aspnetcore/type-based-custom-resolver.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/namespaces/aspnetcore/web-app-example.md b/Documentation/client-snippets/namespaces/aspnetcore/web-app-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/aspnetcore/web-app-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/namespaces/dotnet-client/claims-based.md b/Documentation/client-snippets/namespaces/dotnet-client/claims-based.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/dotnet-client/claims-based.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/namespaces/dotnet-client/default-resolver.md b/Documentation/client-snippets/namespaces/dotnet-client/default-resolver.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/dotnet-client/default-resolver.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/namespaces/dotnet-client/hosted-app-configuration.md b/Documentation/client-snippets/namespaces/dotnet-client/hosted-app-configuration.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/dotnet-client/hosted-app-configuration.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/namespaces/dotnet-client/resolver-shape.md b/Documentation/client-snippets/namespaces/dotnet-client/resolver-shape.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/dotnet-client/resolver-shape.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/namespaces/dotnet-client/tenant-resolver-usage.md b/Documentation/client-snippets/namespaces/dotnet-client/tenant-resolver-usage.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/dotnet-client/tenant-resolver-usage.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/namespaces/dotnet-client/tenant-resolver.md b/Documentation/client-snippets/namespaces/dotnet-client/tenant-resolver.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/namespaces/dotnet-client/tenant-resolver.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 367a9e3b79694e0683b8c784a4b2250f19405501 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:13:50 +0200 Subject: [PATCH 05/39] Declare compliance metadata on the JsonSchema type The generator wrote a compliance array through a (schema as Record) cast because JsonSchema had no field for it. Add ComplianceSchemaMetadata and a typed compliance property so the cast can be dropped. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- Source/schemas/JsonSchema.ts | 16 ++++++++++++++++ Source/schemas/index.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Source/schemas/JsonSchema.ts b/Source/schemas/JsonSchema.ts index 3e958fb..5d591b1 100644 --- a/Source/schemas/JsonSchema.ts +++ b/Source/schemas/JsonSchema.ts @@ -1,6 +1,21 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +/** + * Represents a single compliance classification recorded on a schema node. + */ +export interface ComplianceSchemaMetadata { + /** + * The type of compliance metadata (e.g. 'PII'). + */ + metadataType: string; + + /** + * Any additional details - can be empty. + */ + details: string; +} + /** * Represents a JSON Schema object. */ @@ -15,4 +30,5 @@ export type JsonSchema = { items?: JsonSchema; additionalProperties?: boolean | JsonSchema; enum?: Array; + compliance?: ComplianceSchemaMetadata[]; }; diff --git a/Source/schemas/index.ts b/Source/schemas/index.ts index 96d6fc0..88ba468 100644 --- a/Source/schemas/index.ts +++ b/Source/schemas/index.ts @@ -3,4 +3,4 @@ export { JsonSchemaGenerator } from './JsonSchemaGenerator'; export { jsonSchemaProperty, getTrackedJsonSchemaProperties } from './jsonSchemaProperty'; -export type { JsonSchema } from './JsonSchema'; +export type { ComplianceSchemaMetadata, JsonSchema } from './JsonSchema'; From afdacaaf10ee906391060cb99858751809add456 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:13:59 +0200 Subject: [PATCH 06/39] Fix PII compliance metadata to cover declaring class and leaves Mirrors Cratis.Chronicle.Schemas.JsonSchemaGenerator (the C# gold standard) in three ways addComplianceMetadata did not: - A class-level @pii() only fed getMetadataForType() with the property's own type, never with the declaring class, so "@pii() class Foo { a = ''; b = ''; }" produced a schema with zero compliance entries and every field was persisted in the clear. collectComplianceMetadata() now also consults target.constructor. - Compliance metadata was written onto whatever node addComplianceMetadata was given, including an object node. Chronicle then stores one ciphertext string where the schema says "object", and release hands the read model a string instead of an object. addComplianceMetadataToSchema now descends into schema.properties and lands metadata on the leaves, de-duplicating by metadata type the way HasMetadataOfType does in C#. - An array whose element is a ConceptAs lost its classification: mapRuntimeTypeToSchema() always emitted a bare `{type: 'object'}` item schema. It now resolves the element type from a `@field(Array, { genericArguments: [...] })` declaration and carries the element concept's own compliance metadata onto `items`, mirroring C#'s explicit enumerable-of-concept branch (including leaving the array container itself uncoerced into compliance, since coarse collection-level encryption is a separate, established behavior). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- Source/schemas/JsonSchemaGenerator.ts | 172 ++++++++++++++++++++++---- 1 file changed, 145 insertions(+), 27 deletions(-) diff --git a/Source/schemas/JsonSchemaGenerator.ts b/Source/schemas/JsonSchemaGenerator.ts index 2437339..47682d1 100644 --- a/Source/schemas/JsonSchemaGenerator.ts +++ b/Source/schemas/JsonSchemaGenerator.ts @@ -2,9 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import 'reflect-metadata'; -import { ConceptAs, Guid } from '@cratis/fundamentals'; -import { JsonSchema } from './JsonSchema'; +import { ConceptAs, Constructor, Fields, Guid } from '@cratis/fundamentals'; +import { ComplianceSchemaMetadata, JsonSchema } from './JsonSchema'; import { TypeIntrospector } from '../types'; +import { ComplianceMetadata } from '../compliance/ComplianceMetadata'; import { ComplianceMetadataResolver } from '../compliance/ComplianceMetadataResolver'; /** @@ -48,12 +49,17 @@ export class JsonSchemaGenerator { const prototype = target.prototype; for (const [memberName, memberType] of membersToUse.entries()) { - const propertySchema = this.mapRuntimeTypeToSchema(memberType); + const propertySchema = this.mapRuntimeTypeToSchema(memberType, target, memberName); // Only include properties whose type was resolved. An empty schema ({}) means // the runtime type was unavailable (e.g. esbuild/tsx omits design:paramtypes). if (Object.keys(propertySchema).length > 0) { - // Add compliance metadata to property schema if present (both property and type-level) - this.addComplianceMetadata(propertySchema, prototype, memberName, memberType); + // An array of concept elements resolves and applies its own item-level compliance + // inside mapRuntimeTypeToSchema - the general property/type/declaring-class walk + // below is skipped for it, mirroring the C# generator's enumerable-of-concept branch. + if (!this.isConceptArrayMember(target, memberName, memberType)) { + const metadata = this.collectComplianceMetadata(prototype, memberName, memberType); + this.addComplianceMetadataToSchema(propertySchema, metadata); + } schemaProperties[memberName] = propertySchema; } } @@ -72,7 +78,7 @@ export class JsonSchemaGenerator { }; } - private static mapRuntimeTypeToSchema(runtimeType: Function | undefined): JsonSchema { + private static mapRuntimeTypeToSchema(runtimeType: Function | undefined, declaringType?: Function, propertyName?: string): JsonSchema { const knownTypeFormat = this.getKnownTypeFormat(runtimeType); if (knownTypeFormat) { return knownTypeFormat; @@ -91,7 +97,7 @@ export class JsonSchemaGenerator { } if (runtimeType === Array) { - return { type: 'array', items: { type: 'object' } }; + return this.mapArrayTypeToSchema(declaringType, propertyName); } if (!runtimeType) { @@ -115,6 +121,63 @@ export class JsonSchemaGenerator { return { type: 'object' }; } + /** + * Maps an array-typed member to a schema, resolving the element type from a + * `@field(Array, { genericArguments: [ItemType] })` declaration when present. + * @param declaringType - The class constructor that declares the array property. + * @param propertyName - The array property name. + * @returns The array schema, with the element's own compliance metadata carried onto `items` when the element is a PII concept. + */ + private static mapArrayTypeToSchema(declaringType: Function | undefined, propertyName: string | undefined): JsonSchema { + const elementType = this.getArrayElementType(declaringType, propertyName); + + // An array whose element is a ConceptAs loses its classification the moment it is put in + // a list unless the element concept's own compliance metadata is carried onto the item schema - + // a value that would be encrypted as a scalar would otherwise be persisted in the clear as a + // list element. Mirrors the C# generator's explicit enumerable-of-concept branch. + if (elementType && this.isConceptAs(elementType)) { + const itemSchema = this.mapRuntimeTypeToSchema(elementType); + const metadata = ComplianceMetadataResolver.getMetadataForType(elementType); + this.addComplianceMetadataToSchema(itemSchema, metadata); + return { type: 'array', items: itemSchema }; + } + + return { type: 'array', items: { type: 'object' } }; + } + + /** + * Resolves the element type of an array property from its `@field(Array, { genericArguments: [...] })` + * declaration. TypeScript erases generic type arguments at runtime, so without an explicit + * `@field` declaration the element type cannot be recovered. + * @param declaringType - The class constructor that declares the array property. + * @param propertyName - The array property name. + * @returns The element type constructor, or undefined when it cannot be resolved. + */ + private static getArrayElementType(declaringType: Function | undefined, propertyName: string | undefined): Function | undefined { + if (!declaringType || !propertyName) { + return undefined; + } + + const field = Fields.getFieldsForType(declaringType as Constructor).find(candidate => candidate.name === propertyName); + return field?.genericArguments?.[0]; + } + + /** + * Checks whether a member is an array whose element type is a ConceptAs. + * @param declaringType - The class constructor that declares the property. + * @param propertyName - The property name. + * @param runtimeType - The member's reflected runtime type. + * @returns True when the member is an array of concept elements; false otherwise. + */ + private static isConceptArrayMember(declaringType: Function, propertyName: string, runtimeType: Function | undefined): boolean { + if (runtimeType !== Array) { + return false; + } + + const elementType = this.getArrayElementType(declaringType, propertyName); + return elementType !== undefined && this.isConceptAs(elementType); + } + private static isConceptAs(runtimeType: Function): boolean { let current: Function | null = runtimeType; while (current && current !== Function.prototype) { @@ -150,35 +213,90 @@ export class JsonSchemaGenerator { } /** - * Adds compliance metadata to a property schema if the property has compliance decorators. - * Also checks if the property's type itself is marked as PII (e.g., ConceptAs types). - * @param schema - The property schema to add compliance metadata to. - * @param target - The class prototype. + * Collects compliance metadata for a property from every source C# resolves PII from: the + * property itself, its declaring class, and its own type (the concept case). + * @param target - The class prototype the property is declared on. * @param propertyKey - The property name. + * @param propertyType - The property's runtime type, when resolved. + * @returns The collected compliance metadata, in property → declaring-class → type order. */ - private static addComplianceMetadata(schema: JsonSchema, target: object, propertyKey: string, propertyType?: Function): void { - const complianceArray: Array<{ metadataType: string; details: string }> = []; + private static collectComplianceMetadata(target: object, propertyKey: string, propertyType?: Function): ComplianceMetadata[] { + const metadata: ComplianceMetadata[] = []; - // Check for property-level compliance decorators + // Property-level compliance decorator. if (ComplianceMetadataResolver.hasMetadataFor(target, propertyKey)) { - const propertyMetadata = ComplianceMetadataResolver.getMetadataFor(target, propertyKey); - complianceArray.push(...propertyMetadata.map(metadata => ({ - metadataType: metadata.metadataType.value.toString(), - details: metadata.details - }))); + metadata.push(...ComplianceMetadataResolver.getMetadataFor(target, propertyKey)); + } + + // Declaring class-level compliance decorator - a class-level @pii() marks every one of + // its own properties, the same way C#'s PIIMetadataProvider checks property.DeclaringType. + const declaringClass = (target as { constructor?: Function }).constructor; + if (declaringClass) { + metadata.push(...ComplianceMetadataResolver.getMetadataForType(declaringClass)); } - // Check for type-level compliance decorators (e.g., @pii on ConceptAs) + // Type-level compliance decorator on the property's own type (e.g., @pii on a ConceptAs). if (propertyType) { - const typeMetadata = ComplianceMetadataResolver.getMetadataForType(propertyType); - complianceArray.push(...typeMetadata.map(metadata => ({ - metadataType: metadata.metadataType.value.toString(), - details: metadata.details - }))); + metadata.push(...ComplianceMetadataResolver.getMetadataForType(propertyType)); + } + + return metadata; + } + + /** + * Adds compliance metadata to a schema node, descending into an object's properties so that + * the metadata always lands on the leaves that actually hold a value. + * @param schema - The schema node to add to. + * @param metadata - The compliance metadata to add. + * @remarks + * A compliance marker can be declared on something that is not a single value: a `@pii()` on a + * composite value-object type, or on a property whose type is such an object. Compliance is + * applied per value, so leaving the marker on the container would make Chronicle hand the whole + * JSON object to the value handler and store one opaque ciphertext string where the schema still + * says "object". Releasing that gives back a string, not an object, and the read model then + * fails to materialize. Pushing the metadata down to every leaf keeps encryption symmetric with + * the release walk, keeps each value independently encrypted, and preserves the document shape. + * + * An array-typed node is deliberately left as a container: coarse compliance on a whole + * collection is an established, separately handled behavior (the collection is blob-encrypted + * and its shape restored on release). + */ + private static addComplianceMetadataToSchema(schema: JsonSchema, metadata: ComplianceMetadata[]): void { + if (metadata.length === 0) { + return; } - if (complianceArray.length > 0) { - (schema as Record).compliance = complianceArray; + if (schema.properties && Object.keys(schema.properties).length > 0) { + for (const propertySchema of Object.values(schema.properties)) { + this.addComplianceMetadataToSchema(propertySchema, metadata); + } + return; } + + const compliance = schema.compliance ?? []; + for (const item of metadata) { + const metadataType = item.metadataType.value.toString(); + if (!this.hasComplianceMetadataOfType(compliance, metadataType)) { + compliance.push({ metadataType, details: item.details }); + } + } + + if (compliance.length > 0) { + schema.compliance = compliance; + } + } + + /** + * Checks whether a compliance array already carries metadata of a given type. + * @param compliance - The compliance array to check. + * @param metadataType - The metadata type to look for. + * @returns True when the metadata type is already present, false if not. + * @remarks + * A leaf can be reached by more than one marker — for example a `@pii()` concept inside a value + * object whose type is itself marked `@pii()`. Recording the same metadata type twice adds + * nothing and makes the generated schema noisier to read and to diff. + */ + private static hasComplianceMetadataOfType(compliance: ComplianceSchemaMetadata[], metadataType: string): boolean { + return compliance.some(item => item.metadataType === metadataType); } } From 28e3e07f555a4bfdf31b1ad6eb9eabd77fe20d06 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:14:03 +0200 Subject: [PATCH 07/39] Add specs for JsonSchemaGenerator PII compliance metadata Covers class-level @pii() marking every property, property-level @pii(), concept-typed @pii() on both an event and a read model, class-level combined with property-level producing no duplicate compliance entries, a property-level @pii() on a nested composite value object descending to its leaves, and an array whose element is a PII concept carrying its classification onto the item schema without leaving coarse compliance on the array container. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- Source/schemas/JsonSchemaGenerator.spec.ts | 195 +++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 Source/schemas/JsonSchemaGenerator.spec.ts diff --git a/Source/schemas/JsonSchemaGenerator.spec.ts b/Source/schemas/JsonSchemaGenerator.spec.ts new file mode 100644 index 0000000..55dc09a --- /dev/null +++ b/Source/schemas/JsonSchemaGenerator.spec.ts @@ -0,0 +1,195 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { ConceptAs, field } from '@cratis/fundamentals'; +import { describe, expect, it } from 'vitest'; +import { pii } from '../compliance/pii'; +import { eventType, getEventTypeJsonSchemaFor } from '../events/eventTypeDecorator'; +import { getReadModelMetadata, readModel } from '../readModels/readModel'; +import { JsonSchema } from './JsonSchema'; + +// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these +// fixtures don't depend on the test runner's decorator-syntax support - they exercise exactly +// the same decorator functions and metadata storage that `@decorator` syntax would invoke. +// +// Order matters here in a way it would not for a real `@decorator` stack: class decorators +// apply bottom-to-top, so `pii()` must run - and therefore be called - before `readModel()`/ +// `eventType()` so the compliance metadata already exists when the schema is generated. + +function schemaFor(target: Function): JsonSchema { + return getReadModelMetadata(target)!.schema; +} + +describe('JsonSchemaGenerator', () => { + describe('when a class is marked @pii() at the class level', () => { + class PersonProfile { + name = ''; + email = ''; + } + pii()(PersonProfile); + readModel()(PersonProfile); + + const schema = schemaFor(PersonProfile); + + it('should mark every one of its own properties as PII', () => { + expect(schema.properties!.name.compliance).toEqual([{ metadataType: 'PII', details: '' }]); + expect(schema.properties!.email.compliance).toEqual([{ metadataType: 'PII', details: '' }]); + }); + }); + + describe('when only a single property is marked @pii()', () => { + class Contact { + name = ''; + ssn = ''; + } + pii('Social security number')(Contact.prototype, 'ssn'); + readModel()(Contact); + + const schema = schemaFor(Contact); + + it('should mark the decorated property as PII', () => { + expect(schema.properties!.ssn.compliance).toEqual([{ metadataType: 'PII', details: 'Social security number' }]); + }); + + it('should leave the other property without compliance metadata', () => { + expect(schema.properties!.name.compliance).toBeUndefined(); + }); + }); + + describe('when a property is typed as a ConceptAs marked @pii()', () => { + class EmailAddress extends ConceptAs { + constructor(value: string) { + super(value); + } + } + pii('Email address')(EmailAddress); + + describe('on a read model', () => { + class Customer { + email: EmailAddress = new EmailAddress(''); + } + readModel()(Customer); + + const schema = schemaFor(Customer); + + it('should mark the concept-typed property as PII', () => { + expect(schema.properties!.email.compliance).toEqual([{ metadataType: 'PII', details: 'Email address' }]); + }); + + it('should describe the concept as its underlying primitive type', () => { + expect(schema.properties!.email.type).toBe('string'); + }); + }); + + describe('on an event', () => { + class CustomerRegistered { + email: EmailAddress = new EmailAddress(''); + } + eventType()(CustomerRegistered); + + const schema = getEventTypeJsonSchemaFor(CustomerRegistered); + + it('should mark the concept-typed property as PII', () => { + expect(schema.properties!.email.compliance).toEqual([{ metadataType: 'PII', details: 'Email address' }]); + }); + }); + }); + + describe('when a property-level @pii() combines with a class-level @pii()', () => { + class Employee { + name = ''; + email = ''; + } + pii()(Employee.prototype, 'name'); + pii('Every field on this record is personal')(Employee); + readModel()(Employee); + + const schema = schemaFor(Employee); + + it('should not duplicate the compliance entry on the property carrying both markers', () => { + expect(schema.properties!.name.compliance).toHaveLength(1); + }); + + it('should keep the property-level details when both sources apply', () => { + expect(schema.properties!.name.compliance).toEqual([{ metadataType: 'PII', details: '' }]); + }); + + it('should still mark the property that only carries the class-level marker', () => { + expect(schema.properties!.email.compliance).toEqual([{ metadataType: 'PII', details: 'Every field on this record is personal' }]); + }); + }); + + describe('when a property typed as a nested composite value object is marked @pii()', () => { + class ContactDetails { + phone = ''; + fax = ''; + } + class Vendor { + contact: ContactDetails = new ContactDetails(); + } + pii('Vendor contact information')(Vendor.prototype, 'contact'); + readModel()(Vendor); + + const schema = schemaFor(Vendor); + + it('should push the compliance metadata down onto every leaf property', () => { + expect(schema.properties!.contact.properties!.phone.compliance).toEqual([{ metadataType: 'PII', details: 'Vendor contact information' }]); + expect(schema.properties!.contact.properties!.fax.compliance).toEqual([{ metadataType: 'PII', details: 'Vendor contact information' }]); + }); + + it('should not leave compliance metadata on the container node itself', () => { + expect(schema.properties!.contact.compliance).toBeUndefined(); + }); + }); + + describe('when an array element is a ConceptAs marked @pii()', () => { + class RequirementCode extends ConceptAs { + constructor(value: string) { + super(value); + } + } + pii('Sensitive requirement code')(RequirementCode); + + class Contract { + codes: RequirementCode[] = []; + } + field(Array, { enumerable: true, genericArguments: [RequirementCode] })(Contract.prototype, 'codes'); + readModel()(Contract); + + const schema = schemaFor(Contract); + + it('should describe the property as an array', () => { + expect(schema.properties!.codes.type).toBe('array'); + }); + + it('should carry the element concept compliance metadata onto the item schema', () => { + expect(schema.properties!.codes.items!.compliance).toEqual([{ metadataType: 'PII', details: 'Sensitive requirement code' }]); + }); + + it('should describe the item as the concept underlying primitive type', () => { + expect(schema.properties!.codes.items!.type).toBe('string'); + }); + + it('should not leave coarse compliance metadata on the array container itself', () => { + expect(schema.properties!.codes.compliance).toBeUndefined(); + }); + }); + + describe('when an array element is not a ConceptAs', () => { + class PlainItem { + value = ''; + } + class Basket { + items: PlainItem[] = []; + } + field(Array, { enumerable: true, genericArguments: [PlainItem] })(Basket.prototype, 'items'); + readModel()(Basket); + + const schema = schemaFor(Basket); + + it('should fall back to an opaque object item schema', () => { + expect(schema.properties!.items.items).toEqual({ type: 'object' }); + }); + }); +}); From a5b9adcc55c10943bcdc00946292cfe1e91a61e5 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:14:08 +0200 Subject: [PATCH 08/39] Reject @pii() on the event source identifier property C# throws PIINotSupportedOnEventSourceId when [PII] is applied to a type deriving from EventSourceId/EventSourceId, because encrypting the event source identifier makes its own decryption key unfindable. TypeScript has no equivalent identity type - the event source identifier is always the conventional 'eventSourceId' property (see EventForEventSourceId, EventContext) - so guard that property instead: pii() now throws PIINotSupportedOnEventSourceId when applied to a property named 'eventSourceId'. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../PIINotSupportedOnEventSourceId.ts | 21 +++++++++++ Source/compliance/index.ts | 1 + Source/compliance/pii.spec.ts | 37 +++++++++++++++++++ Source/compliance/pii.ts | 17 ++++++++- 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 Source/compliance/PIINotSupportedOnEventSourceId.ts create mode 100644 Source/compliance/pii.spec.ts diff --git a/Source/compliance/PIINotSupportedOnEventSourceId.ts b/Source/compliance/PIINotSupportedOnEventSourceId.ts new file mode 100644 index 0000000..9b04e01 --- /dev/null +++ b/Source/compliance/PIINotSupportedOnEventSourceId.ts @@ -0,0 +1,21 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/** + * Error thrown when the {@link pii} decorator is applied to the `eventSourceId` property. + */ +export class PIINotSupportedOnEventSourceId extends Error { + /** + * Initializes a new instance of the {@link PIINotSupportedOnEventSourceId} class. + * @param typeName - The name of the type the decorator was applied to. + */ + constructor(typeName: string) { + super( + `The @pii() decorator cannot be applied to 'eventSourceId' on '${typeName}' because it is the event ` + + 'source identifier. Event source identifiers cannot be encrypted as they are required for event ' + + 'correlation. If the identifier itself is sensitive, use a non-sensitive surrogate value as the event ' + + 'source identifier and store the sensitive value in a separate property marked with @pii().' + ); + this.name = 'PIINotSupportedOnEventSourceId'; + } +} diff --git a/Source/compliance/index.ts b/Source/compliance/index.ts index afd6f48..582e833 100644 --- a/Source/compliance/index.ts +++ b/Source/compliance/index.ts @@ -4,6 +4,7 @@ export { ComplianceMetadataType } from './ComplianceMetadataType'; export type { ComplianceMetadata } from './ComplianceMetadata'; export { pii, getPIIMetadata, hasPIIMetadata, getTypePIIMetadata, isPII } from './pii'; +export { PIINotSupportedOnEventSourceId } from './PIINotSupportedOnEventSourceId'; export { ComplianceMetadataResolver } from './ComplianceMetadataResolver'; export type { IPIIManager } from './IPIIManager'; export { PIIManager } from './PIIManager'; diff --git a/Source/compliance/pii.spec.ts b/Source/compliance/pii.spec.ts new file mode 100644 index 0000000..f4eeaa5 --- /dev/null +++ b/Source/compliance/pii.spec.ts @@ -0,0 +1,37 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { describe, expect, it } from 'vitest'; +import { pii } from './pii'; +import { PIINotSupportedOnEventSourceId } from './PIINotSupportedOnEventSourceId'; + +// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these +// fixtures don't depend on the test runner's decorator-syntax support. + +describe('pii', () => { + describe('when applied to the eventSourceId property', () => { + class SomeEvent { + eventSourceId = ''; + } + + it('should throw PIINotSupportedOnEventSourceId', () => { + expect(() => pii()(SomeEvent.prototype, 'eventSourceId')).toThrow(PIINotSupportedOnEventSourceId); + }); + + it('should describe why the property cannot be encrypted', () => { + expect(() => pii()(SomeEvent.prototype, 'eventSourceId')).toThrow(/event source identifier/); + }); + }); + + describe('when applied to any other property', () => { + class SomeEvent { + eventSourceId = ''; + name = ''; + } + + it('should not throw', () => { + expect(() => pii()(SomeEvent.prototype, 'name')).not.toThrow(); + }); + }); +}); diff --git a/Source/compliance/pii.ts b/Source/compliance/pii.ts index 635542d..bcd7d86 100644 --- a/Source/compliance/pii.ts +++ b/Source/compliance/pii.ts @@ -4,8 +4,12 @@ import 'reflect-metadata'; import type { ComplianceMetadata } from './ComplianceMetadata'; import { ComplianceMetadataType } from './ComplianceMetadataType'; +import { PIINotSupportedOnEventSourceId } from './PIINotSupportedOnEventSourceId'; import { TypeIntrospector } from '../types'; +/** The property name this client uses everywhere for the event source identifier. */ +const EVENT_SOURCE_ID_PROPERTY = 'eventSourceId'; + /** Metadata key for PII decorator on properties. */ const PII_PROPERTY_METADATA_KEY = 'chronicle:compliance:pii:property'; @@ -66,7 +70,18 @@ export function pii(details?: string): PropertyDecorator & ClassDecorator { // Property decorator usage if (propertyKey !== undefined) { const key = propertyKey.toString(); - TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key); + const declaringType = (target as { constructor: Function }).constructor; + + // Encrypting the event source identifier would make its own decryption key + // unfindable - the identifier is required, in the clear, to correlate events and + // look up the key that protects everything else. Mirrors C#'s + // PIINotSupportedOnEventSourceId guard, which throws for the same reason when + // [PII] is applied to an EventSourceId/EventSourceId type. + if (key === EVENT_SOURCE_ID_PROPERTY) { + throw new PIINotSupportedOnEventSourceId(declaringType.name); + } + + TypeIntrospector.trackProperty(declaringType, key); const metadata: ComplianceMetadata = { metadataType: ComplianceMetadataType.PII, details: details ?? '' From 42ed6257aab208f7087afe2a7d3995155cfc7f20 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:14:13 +0200 Subject: [PATCH 09/39] Add IPIIManager.allowNewEncryptionKeyFor C# IPIIManager has AllowNewEncryptionKeyFor(EncryptionKeyIdentifier) alongside DeleteEncryptionKeyFor, to re-authorize a subject's key after a GDPR erasure. The TypeScript IPIIManager only had deleteEncryptionKey. The AllowNewEncryptionKey RPC already exists on the wire (verified against @cratis/chronicle.contracts 17.0.0's generated compliance.ts), so add the request/codec to the hand-rolled ComplianceContracts.ts alongside the existing DeleteEncryptionKeyRequest, and implement allowNewEncryptionKeyFor on PIIManager. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- Source/compliance/ComplianceContracts.ts | 59 ++++++++++++++++++++++++ Source/compliance/IPIIManager.ts | 12 +++++ Source/compliance/PIIManager.spec.ts | 50 ++++++++++++++++++++ Source/compliance/PIIManager.ts | 9 ++++ 4 files changed, 130 insertions(+) create mode 100644 Source/compliance/PIIManager.spec.ts diff --git a/Source/compliance/ComplianceContracts.ts b/Source/compliance/ComplianceContracts.ts index 5faa4c5..0795d13 100644 --- a/Source/compliance/ComplianceContracts.ts +++ b/Source/compliance/ComplianceContracts.ts @@ -33,6 +33,16 @@ export interface DeleteEncryptionKeyRequest { Identifier: string; } +/** + * Request to authorize a new encryption key for a PII encryption key identifier whose key was + * previously erased, so a later lawful lifecycle can protect their data again. + */ +export interface AllowNewEncryptionKeyRequest { + EventStore: string; + Namespace: string; + Identifier: string; +} + /** * An empty protobuf message. */ @@ -49,6 +59,7 @@ export type DeepPartial = T extends Builtin ? T : T extends globalThis.Array< export interface ComplianceClient { release(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; deleteEncryptionKey(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; + allowNewEncryptionKey(request: DeepPartial, options?: CallOptions & CallOptionsExt): Promise; } export const ReleaseRequest = { @@ -181,6 +192,46 @@ export const DeleteEncryptionKeyRequest = { } }; +export const AllowNewEncryptionKeyRequest = { + encode(message: AllowNewEncryptionKeyRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.EventStore !== '') { + writer.uint32(10).string(message.EventStore); + } + if (message.Namespace !== '') { + writer.uint32(18).string(message.Namespace); + } + if (message.Identifier !== '') { + writer.uint32(26).string(message.Identifier); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): AllowNewEncryptionKeyRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message: AllowNewEncryptionKeyRequest = { EventStore: '', Namespace: '', Identifier: '' }; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: message.EventStore = reader.string(); continue; + case 2: message.Namespace = reader.string(); continue; + case 3: message.Identifier = reader.string(); continue; + } + if ((tag & 7) === 4 || tag === 0) break; + reader.skip(tag & 7); + } + return message; + }, + + fromPartial(object: DeepPartial): AllowNewEncryptionKeyRequest { + return { + EventStore: object.EventStore ?? '', + Namespace: object.Namespace ?? '', + Identifier: object.Identifier ?? '' + }; + } +}; + export const Empty = { encode(_: Empty, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { return writer; @@ -224,6 +275,14 @@ export const ComplianceDefinition = { responseType: Empty, responseStream: false as const, options: {} + }, + allowNewEncryptionKey: { + name: 'AllowNewEncryptionKey', + requestType: AllowNewEncryptionKeyRequest, + requestStream: false as const, + responseType: Empty, + responseStream: false as const, + options: {} } } } as const; diff --git a/Source/compliance/IPIIManager.ts b/Source/compliance/IPIIManager.ts index ce3f8a3..8a76a44 100644 --- a/Source/compliance/IPIIManager.ts +++ b/Source/compliance/IPIIManager.ts @@ -14,4 +14,16 @@ export interface IPIIManager { * this operation cannot be undone. */ deleteEncryptionKey(identifier: string): Promise; + + /** + * Authorizes a new encryption key for a PII encryption key identifier whose key was previously + * erased, so a later lawful lifecycle can protect their data again. + * @param identifier - The identifier of the encryption key to authorize a new key for. + * @remarks + * Erasing an identifier removes the key that exists now; it does not ban the identifier + * forever. This creates no key - it lets the next PII value written for the identifier + * provision a fresh, independent one, which can decrypt nothing written before the erasure. + * The erased key itself never comes back, whatever else happens. + */ + allowNewEncryptionKeyFor(identifier: string): Promise; } diff --git a/Source/compliance/PIIManager.spec.ts b/Source/compliance/PIIManager.spec.ts new file mode 100644 index 0000000..5c08427 --- /dev/null +++ b/Source/compliance/PIIManager.spec.ts @@ -0,0 +1,50 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { describe, expect, it, vi } from 'vitest'; +import { ChronicleConnection } from '../connection'; +import { PIIManager } from './PIIManager'; + +function createPIIManager() { + const deleteEncryptionKeyMock = vi.fn().mockResolvedValue(undefined); + const allowNewEncryptionKeyMock = vi.fn().mockResolvedValue(undefined); + const connection = { + compliance: { + deleteEncryptionKey: deleteEncryptionKeyMock, + allowNewEncryptionKey: allowNewEncryptionKeyMock + } + } as unknown as ChronicleConnection; + + const manager = new PIIManager('test-store', 'test-namespace', connection); + return { manager, deleteEncryptionKeyMock, allowNewEncryptionKeyMock }; +} + +describe('PIIManager', () => { + describe('when deleting an encryption key', () => { + it('should call the compliance service with the event store, namespace, and identifier', async () => { + const { manager, deleteEncryptionKeyMock } = createPIIManager(); + + await manager.deleteEncryptionKey('some-subject'); + + expect(deleteEncryptionKeyMock).toHaveBeenCalledWith({ + EventStore: 'test-store', + Namespace: 'test-namespace', + Identifier: 'some-subject' + }); + }); + }); + + describe('when allowing a new encryption key', () => { + it('should call the compliance service with the event store, namespace, and identifier', async () => { + const { manager, allowNewEncryptionKeyMock } = createPIIManager(); + + await manager.allowNewEncryptionKeyFor('some-subject'); + + expect(allowNewEncryptionKeyMock).toHaveBeenCalledWith({ + EventStore: 'test-store', + Namespace: 'test-namespace', + Identifier: 'some-subject' + }); + }); + }); +}); diff --git a/Source/compliance/PIIManager.ts b/Source/compliance/PIIManager.ts index 3d5f7bd..7164f94 100644 --- a/Source/compliance/PIIManager.ts +++ b/Source/compliance/PIIManager.ts @@ -28,4 +28,13 @@ export class PIIManager implements IPIIManager { Identifier: identifier }); } + + /** @inheritdoc */ + async allowNewEncryptionKeyFor(identifier: string): Promise { + await this._connection.compliance.allowNewEncryptionKey({ + EventStore: this._eventStore, + Namespace: this._namespace, + Identifier: identifier + }); + } } From 1610de2b2c7a78d329754c2aeb1c42c4b8b92829 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:14:22 +0200 Subject: [PATCH 10/39] Add missing compliance client-snippets Fills the 16 compliance/** snippet IDs the shared Chronicle docs reference but TypeScript resolved to nothing: pii/import, pii/event-source-id-restriction, pii/value-object-class, pii/nested-value-object, pii-with-concepts/why-concept-level, pii-with-concepts/event-source-id-restriction, pii-with-concepts/surrogate-key, client/concept-usage, client/combining, client/event-source-id-restriction, client/registering-compliance, erasure/delete-key, erasure/allow-new-key, read-models/projection-lineage, read-models/reducer-explicit-pii, read-models/querying. Each is the idiomatic TypeScript equivalent of the C# snippet it mirrors. Two areas have no direct TypeScript equivalent and are written accordingly rather than faked: the event-source-id-restriction snippets use the conventional 'eventSourceId' property (TypeScript has no EventSourceId type to attach the restriction to), and registering-compliance shows that no DI-container step exists - the PII manager is available as soon as an event store is. All snippets pass validate-client-snippets.py, which compiles them for real against the built client. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../compliance/client/combining.md | 19 ++++++++++ .../compliance/client/concept-usage.md | 13 +++++++ .../client/event-source-id-restriction.md | 11 ++++++ .../client/registering-compliance.md | 10 ++++++ .../compliance/erasure/allow-new-key.md | 8 +++++ .../compliance/erasure/delete-key.md | 8 +++++ .../event-source-id-restriction.md | 11 ++++++ .../pii-with-concepts/surrogate-key.md | 16 +++++++++ .../pii-with-concepts/why-concept-level.md | 35 +++++++++++++++++++ .../pii/event-source-id-restriction.md | 11 ++++++ .../client-snippets/compliance/pii/import.md | 3 ++ .../compliance/pii/nested-value-object.md | 23 ++++++++++++ .../compliance/pii/value-object-class.md | 16 +++++++++ .../read-models/projection-lineage.md | 26 ++++++++++++++ .../compliance/read-models/querying.md | 11 ++++++ .../read-models/reducer-explicit-pii.md | 25 +++++++++++++ 16 files changed, 246 insertions(+) create mode 100644 Documentation/client-snippets/compliance/client/combining.md create mode 100644 Documentation/client-snippets/compliance/client/concept-usage.md create mode 100644 Documentation/client-snippets/compliance/client/event-source-id-restriction.md create mode 100644 Documentation/client-snippets/compliance/client/registering-compliance.md create mode 100644 Documentation/client-snippets/compliance/erasure/allow-new-key.md create mode 100644 Documentation/client-snippets/compliance/erasure/delete-key.md create mode 100644 Documentation/client-snippets/compliance/pii-with-concepts/event-source-id-restriction.md create mode 100644 Documentation/client-snippets/compliance/pii-with-concepts/surrogate-key.md create mode 100644 Documentation/client-snippets/compliance/pii-with-concepts/why-concept-level.md create mode 100644 Documentation/client-snippets/compliance/pii/event-source-id-restriction.md create mode 100644 Documentation/client-snippets/compliance/pii/import.md create mode 100644 Documentation/client-snippets/compliance/pii/nested-value-object.md create mode 100644 Documentation/client-snippets/compliance/pii/value-object-class.md create mode 100644 Documentation/client-snippets/compliance/read-models/projection-lineage.md create mode 100644 Documentation/client-snippets/compliance/read-models/querying.md create mode 100644 Documentation/client-snippets/compliance/read-models/reducer-explicit-pii.md diff --git a/Documentation/client-snippets/compliance/client/combining.md b/Documentation/client-snippets/compliance/client/combining.md new file mode 100644 index 0000000..17967e2 --- /dev/null +++ b/Documentation/client-snippets/compliance/client/combining.md @@ -0,0 +1,19 @@ +```typescript +import { eventType, pii } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +@pii() +class ComplianceClientEmailAddress extends ConceptAs { + constructor(value: string) { + super(value); + } +} + +@eventType() +class ComplianceClientCustomerRegistered { + name: ComplianceClientPersonName = new ComplianceClientPersonName(''); // encrypted via concept type + email: ComplianceClientEmailAddress = new ComplianceClientEmailAddress(''); // encrypted via concept type + @pii() phoneNumber = ''; // encrypted via property annotation + country = ''; // plaintext +} +``` diff --git a/Documentation/client-snippets/compliance/client/concept-usage.md b/Documentation/client-snippets/compliance/client/concept-usage.md new file mode 100644 index 0000000..21934ae --- /dev/null +++ b/Documentation/client-snippets/compliance/client/concept-usage.md @@ -0,0 +1,13 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class ComplianceClientEmployeeRegisteredWithConcept { + constructor(readonly name: ComplianceClientPersonName, readonly department: string) {} +} + +@eventType() +class ComplianceClientEmployeeNameChanged { + constructor(readonly newName: ComplianceClientPersonName) {} // also encrypted +} +``` diff --git a/Documentation/client-snippets/compliance/client/event-source-id-restriction.md b/Documentation/client-snippets/compliance/client/event-source-id-restriction.md new file mode 100644 index 0000000..52f40bc --- /dev/null +++ b/Documentation/client-snippets/compliance/client/event-source-id-restriction.md @@ -0,0 +1,11 @@ +```typescript +import { pii } from '@cratis/chronicle'; + +// TypeScript represents the event source identifier as the conventional 'eventSourceId' +// property rather than a dedicated EventSourceId type. Marking it @pii() throws +// PIINotSupportedOnEventSourceId - event source identifiers are required for key lookup and +// cannot be encrypted. +class ComplianceClientCustomerId { + @pii() eventSourceId = ''; +} +``` diff --git a/Documentation/client-snippets/compliance/client/registering-compliance.md b/Documentation/client-snippets/compliance/client/registering-compliance.md new file mode 100644 index 0000000..8cf9e28 --- /dev/null +++ b/Documentation/client-snippets/compliance/client/registering-compliance.md @@ -0,0 +1,10 @@ +```typescript +import { ChronicleClient } from '@cratis/chronicle'; + +// TypeScript has no DI-container registration step for compliance support - the PII manager +// is available automatically as soon as you have an event store, with no separate wiring. +async function getPIIManager(chronicleClient: ChronicleClient) { + const eventStore = await chronicleClient.getEventStore('Sales'); + return eventStore.pii; +} +``` diff --git a/Documentation/client-snippets/compliance/erasure/allow-new-key.md b/Documentation/client-snippets/compliance/erasure/allow-new-key.md new file mode 100644 index 0000000..4e00de3 --- /dev/null +++ b/Documentation/client-snippets/compliance/erasure/allow-new-key.md @@ -0,0 +1,8 @@ +```typescript +import { ChronicleClient } from '@cratis/chronicle'; + +async function allowNewEncryptionKeyForPerson(chronicleClient: ChronicleClient): Promise { + const eventStore = await chronicleClient.getEventStore('Sales'); + await eventStore.pii.allowNewEncryptionKeyFor('person-42'); +} +``` diff --git a/Documentation/client-snippets/compliance/erasure/delete-key.md b/Documentation/client-snippets/compliance/erasure/delete-key.md new file mode 100644 index 0000000..da0526e --- /dev/null +++ b/Documentation/client-snippets/compliance/erasure/delete-key.md @@ -0,0 +1,8 @@ +```typescript +import { ChronicleClient } from '@cratis/chronicle'; + +async function deletePersonEncryptionKey(chronicleClient: ChronicleClient): Promise { + const eventStore = await chronicleClient.getEventStore('Sales'); + await eventStore.pii.deleteEncryptionKey('person-42'); +} +``` diff --git a/Documentation/client-snippets/compliance/pii-with-concepts/event-source-id-restriction.md b/Documentation/client-snippets/compliance/pii-with-concepts/event-source-id-restriction.md new file mode 100644 index 0000000..a7609d5 --- /dev/null +++ b/Documentation/client-snippets/compliance/pii-with-concepts/event-source-id-restriction.md @@ -0,0 +1,11 @@ +```typescript +import { pii } from '@cratis/chronicle'; + +// TypeScript has no dedicated EventSourceId type to mark PII on directly - the event +// source identifier is always the conventional 'eventSourceId' property, and marking it +// @pii() throws PIINotSupportedOnEventSourceId for the same reason C# forbids [PII] on a +// concept deriving from EventSourceId. +class PiiConceptsEmployeeId { + @pii() eventSourceId = ''; +} +``` diff --git a/Documentation/client-snippets/compliance/pii-with-concepts/surrogate-key.md b/Documentation/client-snippets/compliance/pii-with-concepts/surrogate-key.md new file mode 100644 index 0000000..da2540b --- /dev/null +++ b/Documentation/client-snippets/compliance/pii-with-concepts/surrogate-key.md @@ -0,0 +1,16 @@ +```typescript +import { eventType, Guid } from '@cratis/chronicle'; + +// ✅ Surrogate key as event source identifier - TypeScript event source identifiers are +// plain strings, so a randomly generated Guid works well with no dedicated identity type +// required. +function createSurrogateEmployeeId(): string { + return Guid.create().toString(); +} + +// ✅ Sensitive values stored in PII-marked concept properties instead +@eventType() +class PiiConceptsSurrogateEmployeeRegistered { + constructor(readonly nationalId: PiiConceptsNationalIdNumber, readonly name: PiiConceptsPersonName) {} +} +``` diff --git a/Documentation/client-snippets/compliance/pii-with-concepts/why-concept-level.md b/Documentation/client-snippets/compliance/pii-with-concepts/why-concept-level.md new file mode 100644 index 0000000..b23fe29 --- /dev/null +++ b/Documentation/client-snippets/compliance/pii-with-concepts/why-concept-level.md @@ -0,0 +1,35 @@ +```typescript +import { eventType, pii } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +// ❌ Property-level: requires repetition across every event +@eventType() +class PiiConceptsComparisonEmployeeRegistered { + @pii() name = ''; + department = ''; +} + +@eventType() +class PiiConceptsComparisonEmployeeNameChanged { + @pii() newName = ''; // must remember @pii() again +} + +// ✅ Concept-level: declare once, apply everywhere automatically +@pii() +class PiiConceptsComparisonPersonName extends ConceptAs { + constructor(value: string) { + super(value); + } +} + +@eventType() +class PiiConceptsComparisonEmployeeRegisteredGood { + name: PiiConceptsComparisonPersonName = new PiiConceptsComparisonPersonName(''); // encrypted + department = ''; +} + +@eventType() +class PiiConceptsComparisonEmployeeNameChangedGood { + newName: PiiConceptsComparisonPersonName = new PiiConceptsComparisonPersonName(''); // also encrypted, no extra annotation needed +} +``` diff --git a/Documentation/client-snippets/compliance/pii/event-source-id-restriction.md b/Documentation/client-snippets/compliance/pii/event-source-id-restriction.md new file mode 100644 index 0000000..6796ead --- /dev/null +++ b/Documentation/client-snippets/compliance/pii/event-source-id-restriction.md @@ -0,0 +1,11 @@ +```typescript +import { pii } from '@cratis/chronicle'; + +// TypeScript has no dedicated EventSourceId type - the event source identifier is always +// the conventional 'eventSourceId' property. Marking it @pii() throws +// PIINotSupportedOnEventSourceId at decoration time, for the same reason C# forbids [PII] on +// EventSourceId: encrypting it would make its own decryption key unfindable. +class PiiAttrEmployeeId { + @pii() eventSourceId = ''; +} +``` diff --git a/Documentation/client-snippets/compliance/pii/import.md b/Documentation/client-snippets/compliance/pii/import.md new file mode 100644 index 0000000..d8ba6af --- /dev/null +++ b/Documentation/client-snippets/compliance/pii/import.md @@ -0,0 +1,3 @@ +```typescript +import { pii } from '@cratis/chronicle'; +``` diff --git a/Documentation/client-snippets/compliance/pii/nested-value-object.md b/Documentation/client-snippets/compliance/pii/nested-value-object.md new file mode 100644 index 0000000..4298f36 --- /dev/null +++ b/Documentation/client-snippets/compliance/pii/nested-value-object.md @@ -0,0 +1,23 @@ +```typescript +import { pii } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +@pii() +class PiiAttrDateOfBirth extends ConceptAs { + constructor(value: string) { + super(value); + } +} + +// The concept sits one level down, inside a value object. +class PiiAttrVerifiedDateOfBirth { + dateOfBirth: PiiAttrDateOfBirth = new PiiAttrDateOfBirth(''); + verifiedBy = ''; +} + +// Chronicle still finds it: dateOfBirth.dateOfBirth is encrypted, verifiedBy is not. +class PiiAttrExpressVerification { + name = ''; + dateOfBirth: PiiAttrVerifiedDateOfBirth = new PiiAttrVerifiedDateOfBirth(); +} +``` diff --git a/Documentation/client-snippets/compliance/pii/value-object-class.md b/Documentation/client-snippets/compliance/pii/value-object-class.md new file mode 100644 index 0000000..c559178 --- /dev/null +++ b/Documentation/client-snippets/compliance/pii/value-object-class.md @@ -0,0 +1,16 @@ +```typescript +import { pii } from '@cratis/chronicle'; + +// Every value this type holds is personal, so mark the type once. +@pii() +class PiiAttrDiagnosis { + condition = ''; + diagnosedBy = ''; +} + +// Both condition and diagnosedBy are encrypted wherever a PiiAttrDiagnosis appears. +class PiiAttrPatientRecord { + name = ''; + diagnosis: PiiAttrDiagnosis = new PiiAttrDiagnosis(); +} +``` diff --git a/Documentation/client-snippets/compliance/read-models/projection-lineage.md b/Documentation/client-snippets/compliance/read-models/projection-lineage.md new file mode 100644 index 0000000..77e20b6 --- /dev/null +++ b/Documentation/client-snippets/compliance/read-models/projection-lineage.md @@ -0,0 +1,26 @@ +```typescript +import { eventType, fromEvent, pii, readModel } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +@pii() +class ComplianceReadModelsPersonName extends ConceptAs { + constructor(value: string) { + super(value); + } +} + +@eventType() +class ComplianceReadModelsEmployeeRegistered { + constructor(readonly name: ComplianceReadModelsPersonName, readonly department: string) {} +} + +// Chronicle's projection pipeline carries PII lineage automatically from the source event +// property into the read model - no @pii() is needed here even though `name` is a plain +// string. It is still encrypted at rest because it came from a PII-marked event property. +@readModel() +@fromEvent(ComplianceReadModelsEmployeeRegistered) +class ComplianceReadModelsEmployee { + name = ''; + department = ''; +} +``` diff --git a/Documentation/client-snippets/compliance/read-models/querying.md b/Documentation/client-snippets/compliance/read-models/querying.md new file mode 100644 index 0000000..181d96b --- /dev/null +++ b/Documentation/client-snippets/compliance/read-models/querying.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ComplianceReadModelsEmployeeService { + constructor(private readonly eventStore: IEventStore) {} + + getEmployee(id: string): Promise { + return this.eventStore.readModels.getInstanceById(ComplianceReadModelsEmployee, id); + } +} +``` diff --git a/Documentation/client-snippets/compliance/read-models/reducer-explicit-pii.md b/Documentation/client-snippets/compliance/read-models/reducer-explicit-pii.md new file mode 100644 index 0000000..1487c56 --- /dev/null +++ b/Documentation/client-snippets/compliance/read-models/reducer-explicit-pii.md @@ -0,0 +1,25 @@ +```typescript +import { eventType, pii, reducer } from '@cratis/chronicle'; + +@eventType() +class ComplianceReadModelsPatientAdmitted { + constructor(readonly name: string, readonly admittedAt: Date) {} +} + +// Reducer-backed read models do not inherit PII lineage from the source event automatically - +// mark the property explicitly. +class ComplianceReadModelsPatientSummary { + @pii() name = ''; + lastAdmittedAt = new Date(); +} + +@reducer('PatientSummaryReducer', undefined, ComplianceReadModelsPatientSummary) +class ComplianceReadModelsPatientSummaryReducer { + async patientAdmitted( + event: ComplianceReadModelsPatientAdmitted, + current?: ComplianceReadModelsPatientSummary + ): Promise { + return { name: event.name, lastAdmittedAt: event.admittedAt }; + } +} +``` From 77f0bdbe7adc07d0071e8de98dd2ae6d01e2d4b1 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:16:02 +0200 Subject: [PATCH 11/39] Add TypeScript snippets for get-started docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../client-snippets/get-started/aspnetcore/endpoint.md | 3 +++ .../get-started/aspnetcore/mongo-registration.md | 3 +++ .../get-started/aspnetcore/register-artifact.md | 3 +++ .../get-started/aspnetcore/register-by-convention.md | 3 +++ .../client-snippets/get-started/aspnetcore/register.md | 3 +++ .../get-started/choose-hosting-model/basic-apphost.md | 3 +++ .../get-started/choose-hosting-model/mongo-database.md | 3 +++ .../get-started/choose-hosting-model/postgres-database.md | 3 +++ .../get-started/choose-hosting-model/sqlite-database.md | 3 +++ .../get-started/choose-hosting-model/sqlserver-database.md | 3 +++ .../client-snippets/get-started/common/mongo-query.md | 3 +++ .../client-snippets/get-started/mongodb/conventions.md | 3 +++ .../client-snippets/get-started/worker/mongo-registration.md | 3 +++ .../get-started/worker/register-by-convention.md | 3 +++ Documentation/client-snippets/get-started/worker/register.md | 3 +++ .../client-snippets/get-started/worker/worker-service.md | 3 +++ 16 files changed, 48 insertions(+) create mode 100644 Documentation/client-snippets/get-started/aspnetcore/endpoint.md create mode 100644 Documentation/client-snippets/get-started/aspnetcore/mongo-registration.md create mode 100644 Documentation/client-snippets/get-started/aspnetcore/register-artifact.md create mode 100644 Documentation/client-snippets/get-started/aspnetcore/register-by-convention.md create mode 100644 Documentation/client-snippets/get-started/aspnetcore/register.md create mode 100644 Documentation/client-snippets/get-started/choose-hosting-model/basic-apphost.md create mode 100644 Documentation/client-snippets/get-started/choose-hosting-model/mongo-database.md create mode 100644 Documentation/client-snippets/get-started/choose-hosting-model/postgres-database.md create mode 100644 Documentation/client-snippets/get-started/choose-hosting-model/sqlite-database.md create mode 100644 Documentation/client-snippets/get-started/choose-hosting-model/sqlserver-database.md create mode 100644 Documentation/client-snippets/get-started/common/mongo-query.md create mode 100644 Documentation/client-snippets/get-started/mongodb/conventions.md create mode 100644 Documentation/client-snippets/get-started/worker/mongo-registration.md create mode 100644 Documentation/client-snippets/get-started/worker/register-by-convention.md create mode 100644 Documentation/client-snippets/get-started/worker/register.md create mode 100644 Documentation/client-snippets/get-started/worker/worker-service.md diff --git a/Documentation/client-snippets/get-started/aspnetcore/endpoint.md b/Documentation/client-snippets/get-started/aspnetcore/endpoint.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/aspnetcore/endpoint.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/aspnetcore/mongo-registration.md b/Documentation/client-snippets/get-started/aspnetcore/mongo-registration.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/aspnetcore/mongo-registration.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/aspnetcore/register-artifact.md b/Documentation/client-snippets/get-started/aspnetcore/register-artifact.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/aspnetcore/register-artifact.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/aspnetcore/register-by-convention.md b/Documentation/client-snippets/get-started/aspnetcore/register-by-convention.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/aspnetcore/register-by-convention.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/aspnetcore/register.md b/Documentation/client-snippets/get-started/aspnetcore/register.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/aspnetcore/register.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/choose-hosting-model/basic-apphost.md b/Documentation/client-snippets/get-started/choose-hosting-model/basic-apphost.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/choose-hosting-model/basic-apphost.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/choose-hosting-model/mongo-database.md b/Documentation/client-snippets/get-started/choose-hosting-model/mongo-database.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/choose-hosting-model/mongo-database.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/choose-hosting-model/postgres-database.md b/Documentation/client-snippets/get-started/choose-hosting-model/postgres-database.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/choose-hosting-model/postgres-database.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/choose-hosting-model/sqlite-database.md b/Documentation/client-snippets/get-started/choose-hosting-model/sqlite-database.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/choose-hosting-model/sqlite-database.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/choose-hosting-model/sqlserver-database.md b/Documentation/client-snippets/get-started/choose-hosting-model/sqlserver-database.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/choose-hosting-model/sqlserver-database.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/common/mongo-query.md b/Documentation/client-snippets/get-started/common/mongo-query.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/common/mongo-query.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/mongodb/conventions.md b/Documentation/client-snippets/get-started/mongodb/conventions.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/mongodb/conventions.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/worker/mongo-registration.md b/Documentation/client-snippets/get-started/worker/mongo-registration.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/worker/mongo-registration.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/worker/register-by-convention.md b/Documentation/client-snippets/get-started/worker/register-by-convention.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/worker/register-by-convention.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/worker/register.md b/Documentation/client-snippets/get-started/worker/register.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/worker/register.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/get-started/worker/worker-service.md b/Documentation/client-snippets/get-started/worker/worker-service.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/get-started/worker/worker-service.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From a2e60cb2ebeb42a4964c3edf5319dfba7da7fe59 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:16:02 +0200 Subject: [PATCH 12/39] Add TypeScript snippets for scenarios docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../scenarios/real-time-query/get-all.md | 12 ++++++++++++ .../real-time-query/materialized-page.md | 11 +++++++++++ .../scenarios/real-time-query/observe-page.md | 13 +++++++++++++ .../scenarios/real-time-query/watch-changes.md | 17 +++++++++++++++++ .../scenarios/test-a-slice/append-spec.md | 3 +++ .../scenarios/test-a-slice/constraint-spec.md | 3 +++ .../scenarios/test-a-slice/read-model-spec.md | 3 +++ 7 files changed, 62 insertions(+) create mode 100644 Documentation/client-snippets/scenarios/real-time-query/get-all.md create mode 100644 Documentation/client-snippets/scenarios/real-time-query/materialized-page.md create mode 100644 Documentation/client-snippets/scenarios/real-time-query/observe-page.md create mode 100644 Documentation/client-snippets/scenarios/real-time-query/watch-changes.md create mode 100644 Documentation/client-snippets/scenarios/test-a-slice/append-spec.md create mode 100644 Documentation/client-snippets/scenarios/test-a-slice/constraint-spec.md create mode 100644 Documentation/client-snippets/scenarios/test-a-slice/read-model-spec.md diff --git a/Documentation/client-snippets/scenarios/real-time-query/get-all.md b/Documentation/client-snippets/scenarios/real-time-query/get-all.md new file mode 100644 index 0000000..540c6c3 --- /dev/null +++ b/Documentation/client-snippets/scenarios/real-time-query/get-all.md @@ -0,0 +1,12 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ScenariosQueryOnLoanBooks { + constructor(private readonly store: IEventStore) {} + + async getOnLoan(): Promise { + const books = await this.store.readModels.getInstances(ScenariosQueryBook); + return books.filter(book => book.onLoan); + } +} +``` diff --git a/Documentation/client-snippets/scenarios/real-time-query/materialized-page.md b/Documentation/client-snippets/scenarios/real-time-query/materialized-page.md new file mode 100644 index 0000000..02b4b63 --- /dev/null +++ b/Documentation/client-snippets/scenarios/real-time-query/materialized-page.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ScenariosQueryBookPageService { + constructor(private readonly store: IEventStore) {} + + getPage(): Promise { + return this.store.readModels.materialized.getInstances(ScenariosQueryBook, 0, 20); + } +} +``` diff --git a/Documentation/client-snippets/scenarios/real-time-query/observe-page.md b/Documentation/client-snippets/scenarios/real-time-query/observe-page.md new file mode 100644 index 0000000..703fa40 --- /dev/null +++ b/Documentation/client-snippets/scenarios/real-time-query/observe-page.md @@ -0,0 +1,13 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ScenariosQueryLiveBookPage { + constructor(private readonly store: IEventStore) {} + + async subscribe(updateView: (books: ScenariosQueryBook[]) => void): Promise { + for await (const page of this.store.readModels.materialized.observeInstances(ScenariosQueryBook, 0, 50)) { + updateView(page); + } + } +} +``` diff --git a/Documentation/client-snippets/scenarios/real-time-query/watch-changes.md b/Documentation/client-snippets/scenarios/real-time-query/watch-changes.md new file mode 100644 index 0000000..e3faa9a --- /dev/null +++ b/Documentation/client-snippets/scenarios/real-time-query/watch-changes.md @@ -0,0 +1,17 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ScenariosQueryBookWatcher { + constructor(private readonly store: IEventStore) {} + + async watch(): Promise { + for await (const changeset of this.store.readModels.watch(ScenariosQueryBook)) { + if (changeset.removed) { + continue; + } + + console.log(`${changeset.key}: on loan = ${changeset.readModel.onLoan}`); + } + } +} +``` diff --git a/Documentation/client-snippets/scenarios/test-a-slice/append-spec.md b/Documentation/client-snippets/scenarios/test-a-slice/append-spec.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/scenarios/test-a-slice/append-spec.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/scenarios/test-a-slice/constraint-spec.md b/Documentation/client-snippets/scenarios/test-a-slice/constraint-spec.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/scenarios/test-a-slice/constraint-spec.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/scenarios/test-a-slice/read-model-spec.md b/Documentation/client-snippets/scenarios/test-a-slice/read-model-spec.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/scenarios/test-a-slice/read-model-spec.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 025cdcf7f9d47bb4d0dbac1227b3c8880cb89652 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:16:02 +0200 Subject: [PATCH 13/39] Add TypeScript snippets for tutorial docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../tutorial/read-model/borrowed-books-query.md | 11 +++++++++++ .../client-snippets/tutorial/read-model/query.md | 12 ++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 Documentation/client-snippets/tutorial/read-model/borrowed-books-query.md create mode 100644 Documentation/client-snippets/tutorial/read-model/query.md diff --git a/Documentation/client-snippets/tutorial/read-model/borrowed-books-query.md b/Documentation/client-snippets/tutorial/read-model/borrowed-books-query.md new file mode 100644 index 0000000..c3ec17a --- /dev/null +++ b/Documentation/client-snippets/tutorial/read-model/borrowed-books-query.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class BorrowedBooks { + constructor(private readonly store: IEventStore) {} + + all(): Promise { + return this.store.readModels.getInstances(BorrowedBook); + } +} +``` diff --git a/Documentation/client-snippets/tutorial/read-model/query.md b/Documentation/client-snippets/tutorial/read-model/query.md new file mode 100644 index 0000000..c3ce4d0 --- /dev/null +++ b/Documentation/client-snippets/tutorial/read-model/query.md @@ -0,0 +1,12 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class Books { + constructor(private readonly store: IEventStore) {} + + async onLoan(): Promise { + const books = await this.store.readModels.getInstances(Book); + return books.filter(book => book.onLoan); + } +} +``` From 8b9c97141b3accb499f49ece356f5caca1effe2e Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:24:09 +0200 Subject: [PATCH 14/39] Add TypeScript snippets for events docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../events/appending/occurred.md | 3 ++ .../events/concurrency/append-many.md | 37 +++++++++++++++++++ .../events/concurrency/builder.md | 27 ++++++++++++++ .../events/concurrency/event-types.md | 36 ++++++++++++++++++ .../events/concurrency/first-append-check.md | 3 ++ .../for-event-source-operations.md | 3 ++ .../events/concurrency/handling-violations.md | 3 ++ .../concurrency/source-and-stream-type.md | 27 ++++++++++++++ .../events/concurrency/strategies.md | 3 ++ .../events/concurrency/stream-id.md | 29 +++++++++++++++ .../cross-cutting-properties/provider.md | 3 ++ .../events/event-source-id/conversions.md | 3 ++ .../events/event-source-id/typed.md | 3 ++ .../filtering/by-event-source-type/reactor.md | 3 ++ .../filtering/by-event-source-type/reducer.md | 3 ++ .../filtering/by-event-stream-type/reactor.md | 3 ++ .../filtering/by-event-stream-type/reducer.md | 3 ++ .../filtering/by-tag/multiple-filters.md | 3 ++ .../events/filtering/by-tag/reactor.md | 3 ++ .../events/filtering/by-tag/reducer.md | 3 ++ .../getting-state/tail-for-event-source.md | 28 ++++++++++++++ .../events/getting-state/tail-for-observer.md | 13 +++++++ .../events/redaction/reactor.md | 3 ++ .../events/redaction/reducer.md | 3 ++ .../events/schema-representation/basic.md | 3 ++ .../transactions/ordered-exact-batch.md | 3 ++ 26 files changed, 254 insertions(+) create mode 100644 Documentation/client-snippets/events/appending/occurred.md create mode 100644 Documentation/client-snippets/events/concurrency/append-many.md create mode 100644 Documentation/client-snippets/events/concurrency/builder.md create mode 100644 Documentation/client-snippets/events/concurrency/event-types.md create mode 100644 Documentation/client-snippets/events/concurrency/first-append-check.md create mode 100644 Documentation/client-snippets/events/concurrency/for-event-source-operations.md create mode 100644 Documentation/client-snippets/events/concurrency/handling-violations.md create mode 100644 Documentation/client-snippets/events/concurrency/source-and-stream-type.md create mode 100644 Documentation/client-snippets/events/concurrency/strategies.md create mode 100644 Documentation/client-snippets/events/concurrency/stream-id.md create mode 100644 Documentation/client-snippets/events/cross-cutting-properties/provider.md create mode 100644 Documentation/client-snippets/events/event-source-id/conversions.md create mode 100644 Documentation/client-snippets/events/event-source-id/typed.md create mode 100644 Documentation/client-snippets/events/filtering/by-event-source-type/reactor.md create mode 100644 Documentation/client-snippets/events/filtering/by-event-source-type/reducer.md create mode 100644 Documentation/client-snippets/events/filtering/by-event-stream-type/reactor.md create mode 100644 Documentation/client-snippets/events/filtering/by-event-stream-type/reducer.md create mode 100644 Documentation/client-snippets/events/filtering/by-tag/multiple-filters.md create mode 100644 Documentation/client-snippets/events/filtering/by-tag/reactor.md create mode 100644 Documentation/client-snippets/events/filtering/by-tag/reducer.md create mode 100644 Documentation/client-snippets/events/getting-state/tail-for-event-source.md create mode 100644 Documentation/client-snippets/events/getting-state/tail-for-observer.md create mode 100644 Documentation/client-snippets/events/redaction/reactor.md create mode 100644 Documentation/client-snippets/events/redaction/reducer.md create mode 100644 Documentation/client-snippets/events/schema-representation/basic.md create mode 100644 Documentation/client-snippets/events/transactions/ordered-exact-batch.md diff --git a/Documentation/client-snippets/events/appending/occurred.md b/Documentation/client-snippets/events/appending/occurred.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/appending/occurred.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/concurrency/append-many.md b/Documentation/client-snippets/events/concurrency/append-many.md new file mode 100644 index 0000000..08904b3 --- /dev/null +++ b/Documentation/client-snippets/events/concurrency/append-many.md @@ -0,0 +1,37 @@ +```typescript +import { eventType, EventForEventSourceId, getEventTypeFor, IEventLog } from '@cratis/chronicle'; + +@eventType() +class ConcurrencyMoneyWithdrawnForTransfer { + constructor(readonly amount: number) {} +} + +@eventType() +class ConcurrencyMoneyDepositedForTransfer { + constructor(readonly amount: number) {} +} + +class ConcurrencyTransferService { + constructor(private readonly eventLog: IEventLog) {} + + async transferMoney(fromAccount: string, toAccount: string, amount: number): Promise { + const events: EventForEventSourceId[] = [ + { eventSourceId: fromAccount, event: new ConcurrencyMoneyWithdrawnForTransfer(amount) }, + { eventSourceId: toAccount, event: new ConcurrencyMoneyDepositedForTransfer(amount) } + ]; + + await this.eventLog.appendMany(events, { + concurrencyScopes: { + [fromAccount]: { + sequenceNumber: 50n, + eventTypes: [getEventTypeFor(ConcurrencyMoneyWithdrawnForTransfer)] + }, + [toAccount]: { + sequenceNumber: 25n, + eventTypes: [getEventTypeFor(ConcurrencyMoneyDepositedForTransfer)] + } + } + }); + } +} +``` diff --git a/Documentation/client-snippets/events/concurrency/builder.md b/Documentation/client-snippets/events/concurrency/builder.md new file mode 100644 index 0000000..9d07216 --- /dev/null +++ b/Documentation/client-snippets/events/concurrency/builder.md @@ -0,0 +1,27 @@ +```typescript +import { eventType, getEventTypeFor, IEventLog } from '@cratis/chronicle'; + +@eventType() +class ConcurrencyMoneyDeposited { + constructor(readonly amount: number) {} +} + +@eventType() +class ConcurrencyMoneyWithdrawn { + constructor(readonly amount: number) {} +} + +class ConcurrencyAccountTransactionService { + constructor(private readonly eventLog: IEventLog) {} + + async processTransaction(accountId: string, amount: number): Promise { + await this.eventLog.append(accountId, new ConcurrencyMoneyDeposited(amount), { + concurrencyScope: { + sequenceNumber: 15n, + eventStreamType: 'Transactions', + eventTypes: [getEventTypeFor(ConcurrencyMoneyDeposited), getEventTypeFor(ConcurrencyMoneyWithdrawn)] + } + }); + } +} +``` diff --git a/Documentation/client-snippets/events/concurrency/event-types.md b/Documentation/client-snippets/events/concurrency/event-types.md new file mode 100644 index 0000000..ac1377d --- /dev/null +++ b/Documentation/client-snippets/events/concurrency/event-types.md @@ -0,0 +1,36 @@ +```typescript +import { eventType, getEventTypeFor, IEventLog } from '@cratis/chronicle'; + +@eventType() +class ConcurrencyPaymentProcessed { + constructor(readonly amount: number) {} +} + +@eventType() +class ConcurrencyPaymentFailed { + constructor(readonly amount: number) {} +} + +@eventType() +class ConcurrencyPaymentRefunded { + constructor(readonly amount: number) {} +} + +class ConcurrencyAccountService { + constructor(private readonly eventLog: IEventLog) {} + + async processPayment(accountId: string, amount: number): Promise { + // Only check concurrency for payment-related events + await this.eventLog.append(accountId, new ConcurrencyPaymentProcessed(amount), { + concurrencyScope: { + sequenceNumber: 20n, + eventTypes: [ + getEventTypeFor(ConcurrencyPaymentProcessed), + getEventTypeFor(ConcurrencyPaymentFailed), + getEventTypeFor(ConcurrencyPaymentRefunded) + ] + } + }); + } +} +``` diff --git a/Documentation/client-snippets/events/concurrency/first-append-check.md b/Documentation/client-snippets/events/concurrency/first-append-check.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/concurrency/first-append-check.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/concurrency/for-event-source-operations.md b/Documentation/client-snippets/events/concurrency/for-event-source-operations.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/concurrency/for-event-source-operations.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/concurrency/handling-violations.md b/Documentation/client-snippets/events/concurrency/handling-violations.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/concurrency/handling-violations.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/concurrency/source-and-stream-type.md b/Documentation/client-snippets/events/concurrency/source-and-stream-type.md new file mode 100644 index 0000000..9f9b8ec --- /dev/null +++ b/Documentation/client-snippets/events/concurrency/source-and-stream-type.md @@ -0,0 +1,27 @@ +```typescript +import { eventType, IEventLog } from '@cratis/chronicle'; + +@eventType() +class ConcurrencyAccountSettingsUpdated { + constructor(readonly settings: string) {} +} + +class ConcurrencyAccountManagementService { + constructor(private readonly eventLog: IEventLog) {} + + async updateAccountSettings(accountId: string, settings: string): Promise { + await this.eventLog.appendMany([{ + eventSourceId: accountId, + event: new ConcurrencyAccountSettingsUpdated(settings), + eventSourceType: 'BankAccount', + eventStreamType: 'AccountManagement' + }], { + concurrencyScope: { + sequenceNumber: 10n, + eventSourceType: 'BankAccount', + eventStreamType: 'AccountManagement' + } + }); + } +} +``` diff --git a/Documentation/client-snippets/events/concurrency/strategies.md b/Documentation/client-snippets/events/concurrency/strategies.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/concurrency/strategies.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/concurrency/stream-id.md b/Documentation/client-snippets/events/concurrency/stream-id.md new file mode 100644 index 0000000..bca7097 --- /dev/null +++ b/Documentation/client-snippets/events/concurrency/stream-id.md @@ -0,0 +1,29 @@ +```typescript +import { eventType, IEventLog } from '@cratis/chronicle'; + +@eventType() +class ConcurrencyMonthlyReportGenerated { + constructor(readonly month: string) {} +} + +class ConcurrencyMonthlyReportService { + constructor(private readonly eventLog: IEventLog) {} + + async generateMonthlyReport(accountId: string, month: Date): Promise { + const monthKey = `${month.getFullYear()}-${String(month.getMonth() + 1).padStart(2, '0')}`; + + await this.eventLog.appendMany([{ + eventSourceId: accountId, + event: new ConcurrencyMonthlyReportGenerated(monthKey), + eventStreamType: 'Reporting', + eventStreamId: monthKey + }], { + concurrencyScope: { + sequenceNumber: 5n, + eventStreamType: 'Reporting', + eventStreamId: monthKey + } + }); + } +} +``` diff --git a/Documentation/client-snippets/events/cross-cutting-properties/provider.md b/Documentation/client-snippets/events/cross-cutting-properties/provider.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/cross-cutting-properties/provider.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/event-source-id/conversions.md b/Documentation/client-snippets/events/event-source-id/conversions.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/event-source-id/conversions.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/event-source-id/typed.md b/Documentation/client-snippets/events/event-source-id/typed.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/event-source-id/typed.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/filtering/by-event-source-type/reactor.md b/Documentation/client-snippets/events/filtering/by-event-source-type/reactor.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/filtering/by-event-source-type/reactor.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/filtering/by-event-source-type/reducer.md b/Documentation/client-snippets/events/filtering/by-event-source-type/reducer.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/filtering/by-event-source-type/reducer.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/filtering/by-event-stream-type/reactor.md b/Documentation/client-snippets/events/filtering/by-event-stream-type/reactor.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/filtering/by-event-stream-type/reactor.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/filtering/by-event-stream-type/reducer.md b/Documentation/client-snippets/events/filtering/by-event-stream-type/reducer.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/filtering/by-event-stream-type/reducer.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/filtering/by-tag/multiple-filters.md b/Documentation/client-snippets/events/filtering/by-tag/multiple-filters.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/filtering/by-tag/multiple-filters.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/filtering/by-tag/reactor.md b/Documentation/client-snippets/events/filtering/by-tag/reactor.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/filtering/by-tag/reactor.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/filtering/by-tag/reducer.md b/Documentation/client-snippets/events/filtering/by-tag/reducer.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/filtering/by-tag/reducer.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/getting-state/tail-for-event-source.md b/Documentation/client-snippets/events/getting-state/tail-for-event-source.md new file mode 100644 index 0000000..964dc37 --- /dev/null +++ b/Documentation/client-snippets/events/getting-state/tail-for-event-source.md @@ -0,0 +1,28 @@ +```typescript +import { eventType, EventSequenceNumber, IEventLog } from '@cratis/chronicle'; + +@eventType() +class GettingStateInventoryAdjusted { + constructor(readonly sku: string, readonly delta: number) {} +} + +@eventType() +class GettingStateInventoryReserved { + constructor(readonly sku: string, readonly quantity: number) {} +} + +class GettingStateInventoryCheckpoint { + constructor(private readonly eventLog: IEventLog) {} + + // Scopes the tail to a specific stream of inventory events. + captureFor(inventoryId: string): Promise { + return this.eventLog.getTailSequenceNumber( + inventoryId, + undefined, + undefined, + undefined, + [GettingStateInventoryAdjusted, GettingStateInventoryReserved] + ); + } +} +``` diff --git a/Documentation/client-snippets/events/getting-state/tail-for-observer.md b/Documentation/client-snippets/events/getting-state/tail-for-observer.md new file mode 100644 index 0000000..7620b5f --- /dev/null +++ b/Documentation/client-snippets/events/getting-state/tail-for-observer.md @@ -0,0 +1,13 @@ +```typescript +import { Constructor } from '@cratis/fundamentals'; +import { EventSequenceNumber, IEventSequence } from '@cratis/chronicle'; + +class GettingStateObserverProgress { + constructor(private readonly eventSequence: IEventSequence) {} + + // Uses the observer's event type filters to compute the relevant tail. + getRelevantTail(observerType: Constructor): Promise { + return this.eventSequence.getTailSequenceNumberForObserver(observerType); + } +} +``` diff --git a/Documentation/client-snippets/events/redaction/reactor.md b/Documentation/client-snippets/events/redaction/reactor.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/redaction/reactor.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/redaction/reducer.md b/Documentation/client-snippets/events/redaction/reducer.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/redaction/reducer.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/schema-representation/basic.md b/Documentation/client-snippets/events/schema-representation/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/schema-representation/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/transactions/ordered-exact-batch.md b/Documentation/client-snippets/events/transactions/ordered-exact-batch.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/transactions/ordered-exact-batch.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From effbde0bc4ac2de66bd3d81038853132c4752e11 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:24:09 +0200 Subject: [PATCH 15/39] Add TypeScript snippets for reducers docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../event-processing/accumulation-pattern.md | 24 ++++++++ .../event-processing/async-patterns.md | 11 ++++ .../event-processing/basic-sync-pattern.md | 6 ++ .../collection-building-pattern.md | 34 +++++++++++ .../conditional-processing-pattern.md | 49 ++++++++++++++++ .../event-processing/event-context-shape.md | 14 +++++ .../reducers/event-processing/first-event.md | 42 ++++++++++++++ .../event-processing/method-discovery.md | 42 ++++++++++++++ .../minimize-object-creation.md | 28 ++++++++++ .../event-processing/pattern-with-context.md | 8 +++ .../event-processing/recording-errors.md | 25 +++++++++ .../event-processing/reuse-collections.md | 30 ++++++++++ .../event-processing/skip-invalid-state.md | 26 +++++++++ .../state-transitions-pattern.md | 56 +++++++++++++++++++ .../time-based-aggregation-pattern.md | 28 ++++++++++ .../event-processing/using-event-context.md | 31 ++++++++++ .../event-sequence/event-log-attribute.md | 24 ++++++++ .../event-sequence-attribute.md | 22 ++++++++ .../automatic-routing.md | 3 + .../observer-level-event-store.md | 3 + .../filtering/by-event-source-type.md | 3 + .../filtering/by-event-stream-type.md | 3 + .../reducers/filtering/by-tag.md | 3 + .../reducers/filtering/combine-filters.md | 3 + .../reducers/filtering/metadata-example.md | 3 + .../reducers/filtering/multiple-tags.md | 3 + .../reducers/filtering/tag-vs-filter.md | 3 + .../getting-started/async-signatures.md | 23 ++++++++ .../getting-started/sync-signatures.md | 25 +++++++++ .../passive-reducers/development-testing.md | 12 ++++ .../passive-reducers/historical-snapshots.md | 16 ++++++ .../passive-reducers/on-demand-reports.md | 36 ++++++++++++ .../passive-reducers/passive-attribute.md | 28 ++++++++++ .../passive-reducers/reducer-attribute.md | 29 ++++++++++ .../passive-reducers/retrieving-state.md | 12 ++++ .../switching-active-passive.md | 12 ++++ .../passive-reducers/temporary-analysis.md | 13 +++++ .../tagging-reducers/mixed-approach.md | 3 + .../multiple-tags-multiple-attributes.md | 3 + .../multiple-tags-single-attribute.md | 3 + .../reducers/tagging-reducers/single-tag.md | 3 + .../tagging-reducers/tag-categories.md | 3 + 42 files changed, 748 insertions(+) create mode 100644 Documentation/client-snippets/reducers/event-processing/accumulation-pattern.md create mode 100644 Documentation/client-snippets/reducers/event-processing/async-patterns.md create mode 100644 Documentation/client-snippets/reducers/event-processing/basic-sync-pattern.md create mode 100644 Documentation/client-snippets/reducers/event-processing/collection-building-pattern.md create mode 100644 Documentation/client-snippets/reducers/event-processing/conditional-processing-pattern.md create mode 100644 Documentation/client-snippets/reducers/event-processing/event-context-shape.md create mode 100644 Documentation/client-snippets/reducers/event-processing/first-event.md create mode 100644 Documentation/client-snippets/reducers/event-processing/method-discovery.md create mode 100644 Documentation/client-snippets/reducers/event-processing/minimize-object-creation.md create mode 100644 Documentation/client-snippets/reducers/event-processing/pattern-with-context.md create mode 100644 Documentation/client-snippets/reducers/event-processing/recording-errors.md create mode 100644 Documentation/client-snippets/reducers/event-processing/reuse-collections.md create mode 100644 Documentation/client-snippets/reducers/event-processing/skip-invalid-state.md create mode 100644 Documentation/client-snippets/reducers/event-processing/state-transitions-pattern.md create mode 100644 Documentation/client-snippets/reducers/event-processing/time-based-aggregation-pattern.md create mode 100644 Documentation/client-snippets/reducers/event-processing/using-event-context.md create mode 100644 Documentation/client-snippets/reducers/event-sequence/event-log-attribute.md create mode 100644 Documentation/client-snippets/reducers/event-sequence/event-sequence-attribute.md create mode 100644 Documentation/client-snippets/reducers/external-event-store-subscriptions/automatic-routing.md create mode 100644 Documentation/client-snippets/reducers/external-event-store-subscriptions/observer-level-event-store.md create mode 100644 Documentation/client-snippets/reducers/filtering/by-event-source-type.md create mode 100644 Documentation/client-snippets/reducers/filtering/by-event-stream-type.md create mode 100644 Documentation/client-snippets/reducers/filtering/by-tag.md create mode 100644 Documentation/client-snippets/reducers/filtering/combine-filters.md create mode 100644 Documentation/client-snippets/reducers/filtering/metadata-example.md create mode 100644 Documentation/client-snippets/reducers/filtering/multiple-tags.md create mode 100644 Documentation/client-snippets/reducers/filtering/tag-vs-filter.md create mode 100644 Documentation/client-snippets/reducers/getting-started/async-signatures.md create mode 100644 Documentation/client-snippets/reducers/getting-started/sync-signatures.md create mode 100644 Documentation/client-snippets/reducers/passive-reducers/development-testing.md create mode 100644 Documentation/client-snippets/reducers/passive-reducers/historical-snapshots.md create mode 100644 Documentation/client-snippets/reducers/passive-reducers/on-demand-reports.md create mode 100644 Documentation/client-snippets/reducers/passive-reducers/passive-attribute.md create mode 100644 Documentation/client-snippets/reducers/passive-reducers/reducer-attribute.md create mode 100644 Documentation/client-snippets/reducers/passive-reducers/retrieving-state.md create mode 100644 Documentation/client-snippets/reducers/passive-reducers/switching-active-passive.md create mode 100644 Documentation/client-snippets/reducers/passive-reducers/temporary-analysis.md create mode 100644 Documentation/client-snippets/reducers/tagging-reducers/mixed-approach.md create mode 100644 Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-multiple-attributes.md create mode 100644 Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-single-attribute.md create mode 100644 Documentation/client-snippets/reducers/tagging-reducers/single-tag.md create mode 100644 Documentation/client-snippets/reducers/tagging-reducers/tag-categories.md diff --git a/Documentation/client-snippets/reducers/event-processing/accumulation-pattern.md b/Documentation/client-snippets/reducers/event-processing/accumulation-pattern.md new file mode 100644 index 0000000..25ac897 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/accumulation-pattern.md @@ -0,0 +1,24 @@ +```typescript +import { eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingMetricRecorded { + constructor(readonly value: number) {} +} + +class EventProcessingStatistics { + sum = 0; + count = 0; + average = 0; +} + +@reducer('', undefined, EventProcessingStatistics) +class EventProcessingStatisticsReducer { + eventProcessingMetricRecorded(event: EventProcessingMetricRecorded, current: EventProcessingStatistics | undefined): EventProcessingStatistics { + const sum = (current?.sum ?? 0) + event.value; + const count = (current?.count ?? 0) + 1; + + return { sum, count, average: sum / count }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/async-patterns.md b/Documentation/client-snippets/reducers/event-processing/async-patterns.md new file mode 100644 index 0000000..b876b56 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/async-patterns.md @@ -0,0 +1,11 @@ +```typescript +import { EventContext } from '@cratis/chronicle'; + +interface EventProcessingAsyncPatterns { + // Async without context + process(event: TEvent, current: TReadModel | undefined): Promise; + + // Async with context + processWithContext(event: TEvent, current: TReadModel | undefined, context: EventContext): Promise; +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/basic-sync-pattern.md b/Documentation/client-snippets/reducers/event-processing/basic-sync-pattern.md new file mode 100644 index 0000000..a575334 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/basic-sync-pattern.md @@ -0,0 +1,6 @@ +```typescript +interface EventProcessingBasicSyncPattern { + // Process event and return new state + process(event: TEvent, current: TReadModel | undefined): TReadModel; +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/collection-building-pattern.md b/Documentation/client-snippets/reducers/event-processing/collection-building-pattern.md new file mode 100644 index 0000000..544cefe --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/collection-building-pattern.md @@ -0,0 +1,34 @@ +```typescript +import { EventContext, eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingCustomerAction { + constructor(readonly type: string, readonly description: string) {} +} + +class EventProcessingActivity { + type = ''; + timestamp = new Date(); + description = ''; +} + +class EventProcessingCustomerActivityLog { + activities: EventProcessingActivity[] = []; +} + +@reducer('', undefined, EventProcessingCustomerActivityLog) +class EventProcessingCustomerActivityLogReducer { + eventProcessingCustomerAction( + event: EventProcessingCustomerAction, + current: EventProcessingCustomerActivityLog | undefined, + context: EventContext + ): EventProcessingCustomerActivityLog { + // Copy rather than mutate — current.activities may still be referenced by a held snapshot + const activities = [...(current?.activities ?? [])]; + + activities.push({ type: event.type, timestamp: context.occurred, description: event.description }); + + return { activities }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/conditional-processing-pattern.md b/Documentation/client-snippets/reducers/event-processing/conditional-processing-pattern.md new file mode 100644 index 0000000..92d766a --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/conditional-processing-pattern.md @@ -0,0 +1,49 @@ +```typescript +import { EventContext, eventType, Guid, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingAccountOpened { + constructor(readonly accountId: Guid) {} +} + +@eventType() +class EventProcessingDepositMade { + constructor(readonly amount: number) {} +} + +@eventType() +class EventProcessingAccountClosed {} + +class EventProcessingAccount { + accountId: Guid = Guid.empty; + balance = 0; + isActive = false; +} + +@reducer('', undefined, EventProcessingAccount) +class EventProcessingAccountReducer { + eventProcessingAccountOpened(event: EventProcessingAccountOpened, current: EventProcessingAccount | undefined): EventProcessingAccount { + return { accountId: event.accountId, balance: 0, isActive: true }; + } + + eventProcessingDepositMade( + event: EventProcessingDepositMade, + current: EventProcessingAccount | undefined, + context: EventContext + ): EventProcessingAccount | undefined { + // Skip if account doesn't exist or is not active + if (!current || !current.isActive) return current; + + return { ...current, balance: current.balance + event.amount }; + } + + eventProcessingAccountClosed( + event: EventProcessingAccountClosed, + current: EventProcessingAccount | undefined + ): EventProcessingAccount | undefined { + if (!current) return undefined; + + return { ...current, isActive: false }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/event-context-shape.md b/Documentation/client-snippets/reducers/event-processing/event-context-shape.md new file mode 100644 index 0000000..b1c1245 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/event-context-shape.md @@ -0,0 +1,14 @@ +```typescript +import { CausationEntry, EventType } from '@cratis/chronicle'; + +// Illustrative subset of the real EventContext shape from '@cratis/chronicle' +interface EventProcessingEventContextShape { + readonly sequenceNumber: bigint; + readonly eventSourceId: string; + readonly eventType: EventType; + readonly occurred: Date; + readonly correlationId: string; + readonly causation: ReadonlyArray; +} +// ... see EventContext for the authoritative member list +``` diff --git a/Documentation/client-snippets/reducers/event-processing/first-event.md b/Documentation/client-snippets/reducers/event-processing/first-event.md new file mode 100644 index 0000000..b059f26 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/first-event.md @@ -0,0 +1,42 @@ +```typescript +import { EventContext, eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingDataRecorded { + constructor(readonly value: number) {} +} + +class EventProcessingAnalytics { + eventCount = 0; + firstEventTime = new Date(); + lastEventTime = new Date(); + totalValue = 0; +} + +@reducer('', undefined, EventProcessingAnalytics) +class EventProcessingAnalyticsReducer { + eventProcessingDataRecorded( + event: EventProcessingDataRecorded, + current: EventProcessingAnalytics | undefined, + context: EventContext + ): EventProcessingAnalytics { + if (!current) { + // First event - initialize state + return { + eventCount: 1, + firstEventTime: context.occurred, + lastEventTime: context.occurred, + totalValue: event.value + }; + } + + // Update existing state + return { + ...current, + eventCount: current.eventCount + 1, + lastEventTime: context.occurred, + totalValue: current.totalValue + event.value + }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/method-discovery.md b/Documentation/client-snippets/reducers/event-processing/method-discovery.md new file mode 100644 index 0000000..474aa18 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/method-discovery.md @@ -0,0 +1,42 @@ +```typescript +import { EventContext, eventType, Guid, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingOrderCreated { + constructor(readonly orderId: Guid) {} +} + +@eventType() +class EventProcessingItemAdded { + constructor(readonly price: number) {} +} + +class EventProcessingOrderSummary { + orderId: Guid = Guid.empty; + total = 0; + lastUpdated = new Date(); +} + +// Method names must be the exact camelCase of the event's class name - +// Chronicle discovers handlers by name, not by parameter type. +@reducer('', undefined, EventProcessingOrderSummary) +class EventProcessingOrderSummaryReducer { + eventProcessingOrderCreated( + event: EventProcessingOrderCreated, + current: EventProcessingOrderSummary | undefined, + context: EventContext + ): EventProcessingOrderSummary { + return { orderId: event.orderId, total: 0, lastUpdated: context.occurred }; + } + + eventProcessingItemAdded( + event: EventProcessingItemAdded, + current: EventProcessingOrderSummary | undefined, + context: EventContext + ): EventProcessingOrderSummary | undefined { + if (!current) return undefined; // Skip if no order exists + + return { ...current, total: current.total + event.price, lastUpdated: context.occurred }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/minimize-object-creation.md b/Documentation/client-snippets/reducers/event-processing/minimize-object-creation.md new file mode 100644 index 0000000..e3dc371 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/minimize-object-creation.md @@ -0,0 +1,28 @@ +```typescript +import { eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingMinimalMetricRecorded { + constructor(readonly value: number) {} +} + +class EventProcessingMinimalStats { + count = 0; + sum = 0; +} + +@reducer('', undefined, EventProcessingMinimalStats) +class EventProcessingMinimalStatsReducer { + // Efficient - only creates a new object when needed + eventProcessingMinimalMetricRecorded( + event: EventProcessingMinimalMetricRecorded, + current: EventProcessingMinimalStats | undefined + ): EventProcessingMinimalStats { + if (!current) { + return { count: 1, sum: event.value }; + } + + return { count: current.count + 1, sum: current.sum + event.value }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/pattern-with-context.md b/Documentation/client-snippets/reducers/event-processing/pattern-with-context.md new file mode 100644 index 0000000..a05f0c8 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/pattern-with-context.md @@ -0,0 +1,8 @@ +```typescript +import { EventContext } from '@cratis/chronicle'; + +interface EventProcessingPatternWithContext { + // Access occurred time, correlation ID, etc. + process(event: TEvent, current: TReadModel | undefined, context: EventContext): TReadModel; +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/recording-errors.md b/Documentation/client-snippets/reducers/event-processing/recording-errors.md new file mode 100644 index 0000000..1ffc4bd --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/recording-errors.md @@ -0,0 +1,25 @@ +```typescript +import { eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingInvalidDataDetected { + constructor(readonly reason: string) {} +} + +class EventProcessingValidationResult { + isValid = true; + errors: string[] = []; +} + +@reducer('', undefined, EventProcessingValidationResult) +class EventProcessingValidationResultReducer { + eventProcessingInvalidDataDetected( + event: EventProcessingInvalidDataDetected, + current: EventProcessingValidationResult | undefined + ): EventProcessingValidationResult { + const errors = [...(current?.errors ?? []), event.reason]; + + return { isValid: false, errors }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/reuse-collections.md b/Documentation/client-snippets/reducers/event-processing/reuse-collections.md new file mode 100644 index 0000000..c51b5a0 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/reuse-collections.md @@ -0,0 +1,30 @@ +```typescript +import { eventType, Guid, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingReuseItemAdded { + constructor(readonly itemId: Guid, readonly name: string) {} +} + +class EventProcessingItem { + itemId: Guid = Guid.empty; + name = ''; +} + +class EventProcessingItemList { + items: EventProcessingItem[] = []; +} + +@reducer('', undefined, EventProcessingItemList) +class EventProcessingItemListReducer { + eventProcessingReuseItemAdded( + event: EventProcessingReuseItemAdded, + current: EventProcessingItemList | undefined + ): EventProcessingItemList { + // Copy rather than mutate current.items directly — a held snapshot may still reference it + const items = [...(current?.items ?? []), { itemId: event.itemId, name: event.name }]; + + return { items }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/skip-invalid-state.md b/Documentation/client-snippets/reducers/event-processing/skip-invalid-state.md new file mode 100644 index 0000000..a971dff --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/skip-invalid-state.md @@ -0,0 +1,26 @@ +```typescript +import { EventContext, eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingSkipItemAdded { + constructor(readonly price: number) {} +} + +class EventProcessingSkipOrderSummary { + total = 0; +} + +@reducer('', undefined, EventProcessingSkipOrderSummary) +class EventProcessingSkipOrderSummaryReducer { + eventProcessingSkipItemAdded( + event: EventProcessingSkipItemAdded, + current: EventProcessingSkipOrderSummary | undefined, + context: EventContext + ): EventProcessingSkipOrderSummary | undefined { + // Can't add items if order doesn't exist + if (!current) return undefined; + + return { total: current.total + event.price }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/state-transitions-pattern.md b/Documentation/client-snippets/reducers/event-processing/state-transitions-pattern.md new file mode 100644 index 0000000..d815566 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/state-transitions-pattern.md @@ -0,0 +1,56 @@ +```typescript +import { EventContext, eventType, Guid, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingOrderCreatedForStatus { + constructor(readonly orderId: Guid) {} +} + +@eventType() +class EventProcessingOrderPaid { + constructor(readonly orderId: Guid) {} +} + +@eventType() +class EventProcessingOrderShipped { + constructor(readonly orderId: Guid) {} +} + +@eventType() +class EventProcessingOrderDelivered { + constructor(readonly orderId: Guid) {} +} + +@eventType() +class EventProcessingOrderCancelled { + constructor(readonly orderId: Guid) {} +} + +class EventProcessingOrderStatus { + state = ''; + lastUpdated = new Date(); +} + +@reducer('', undefined, EventProcessingOrderStatus) +class EventProcessingOrderStatusReducer { + eventProcessingOrderCreatedForStatus(event: EventProcessingOrderCreatedForStatus, current: EventProcessingOrderStatus | undefined, context: EventContext): EventProcessingOrderStatus { + return { state: 'Created', lastUpdated: context.occurred }; + } + + eventProcessingOrderPaid(event: EventProcessingOrderPaid, current: EventProcessingOrderStatus | undefined, context: EventContext): EventProcessingOrderStatus { + return { state: 'Paid', lastUpdated: context.occurred }; + } + + eventProcessingOrderShipped(event: EventProcessingOrderShipped, current: EventProcessingOrderStatus | undefined, context: EventContext): EventProcessingOrderStatus { + return { state: 'Shipped', lastUpdated: context.occurred }; + } + + eventProcessingOrderDelivered(event: EventProcessingOrderDelivered, current: EventProcessingOrderStatus | undefined, context: EventContext): EventProcessingOrderStatus { + return { state: 'Delivered', lastUpdated: context.occurred }; + } + + eventProcessingOrderCancelled(event: EventProcessingOrderCancelled, current: EventProcessingOrderStatus | undefined, context: EventContext): EventProcessingOrderStatus { + return { state: 'Cancelled', lastUpdated: context.occurred }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/time-based-aggregation-pattern.md b/Documentation/client-snippets/reducers/event-processing/time-based-aggregation-pattern.md new file mode 100644 index 0000000..bc32672 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/time-based-aggregation-pattern.md @@ -0,0 +1,28 @@ +```typescript +import { EventContext, eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingHourlyMetricRecorded { + constructor(readonly value: number) {} +} + +class EventProcessingHourlyMetrics { + metricsByHour: Record = {}; +} + +@reducer('', undefined, EventProcessingHourlyMetrics) +class EventProcessingHourlyMetricsReducer { + eventProcessingHourlyMetricRecorded( + event: EventProcessingHourlyMetricRecorded, + current: EventProcessingHourlyMetrics | undefined, + context: EventContext + ): EventProcessingHourlyMetrics { + const metricsByHour = { ...(current?.metricsByHour ?? {}) }; + const hour = context.occurred.getHours(); + + metricsByHour[hour] = (metricsByHour[hour] ?? 0) + event.value; + + return { metricsByHour }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-processing/using-event-context.md b/Documentation/client-snippets/reducers/event-processing/using-event-context.md new file mode 100644 index 0000000..da62506 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-processing/using-event-context.md @@ -0,0 +1,31 @@ +```typescript +import { EventContext, eventType, Guid, reducer } from '@cratis/chronicle'; + +@eventType() +class EventProcessingContextOrderPlaced { + constructor(readonly orderId: Guid, readonly amount: number) {} +} + +class EventProcessingOrderSummaryWithContext { + orderId: Guid = Guid.empty; + total = 0; + placedAt = new Date(); + correlationId = ''; +} + +@reducer('', undefined, EventProcessingOrderSummaryWithContext) +class EventProcessingOrderSummaryWithContextReducer { + eventProcessingContextOrderPlaced( + event: EventProcessingContextOrderPlaced, + current: EventProcessingOrderSummaryWithContext | undefined, + context: EventContext + ): EventProcessingOrderSummaryWithContext { + return { + orderId: event.orderId, + total: event.amount, + placedAt: context.occurred, + correlationId: context.correlationId + }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-sequence/event-log-attribute.md b/Documentation/client-snippets/reducers/event-sequence/event-log-attribute.md new file mode 100644 index 0000000..8ef85b5 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-sequence/event-log-attribute.md @@ -0,0 +1,24 @@ +```typescript +import { EventContext, eventType, Guid, reducer } from '@cratis/chronicle'; + +@eventType() +class EventSequenceLogOrderPlaced { + constructor(readonly orderId: Guid) {} +} + +class EventSequenceLogOrderAudit { + orderId: Guid = Guid.empty; +} + +// No eventSequenceId given - observes the default event log +@reducer('', undefined, EventSequenceLogOrderAudit) +class EventSequenceLocalAuditReducer { + eventSequenceLogOrderPlaced( + event: EventSequenceLogOrderPlaced, + current: EventSequenceLogOrderAudit | undefined, + context: EventContext + ): EventSequenceLogOrderAudit { + return { orderId: event.orderId }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/event-sequence/event-sequence-attribute.md b/Documentation/client-snippets/reducers/event-sequence/event-sequence-attribute.md new file mode 100644 index 0000000..5fc2d59 --- /dev/null +++ b/Documentation/client-snippets/reducers/event-sequence/event-sequence-attribute.md @@ -0,0 +1,22 @@ +```typescript +import { eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class EventSequenceShipmentDispatched { + constructor(readonly trackingNumber: string) {} +} + +class EventSequenceShipmentStatus { + trackingNumber = ''; +} + +@reducer('', 'fulfillment-events', EventSequenceShipmentStatus) +class EventSequenceShipmentReducer { + eventSequenceShipmentDispatched( + event: EventSequenceShipmentDispatched, + current: EventSequenceShipmentStatus | undefined + ): EventSequenceShipmentStatus { + return { trackingNumber: event.trackingNumber }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/external-event-store-subscriptions/automatic-routing.md b/Documentation/client-snippets/reducers/external-event-store-subscriptions/automatic-routing.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/external-event-store-subscriptions/automatic-routing.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/external-event-store-subscriptions/observer-level-event-store.md b/Documentation/client-snippets/reducers/external-event-store-subscriptions/observer-level-event-store.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/external-event-store-subscriptions/observer-level-event-store.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/filtering/by-event-source-type.md b/Documentation/client-snippets/reducers/filtering/by-event-source-type.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/filtering/by-event-source-type.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/filtering/by-event-stream-type.md b/Documentation/client-snippets/reducers/filtering/by-event-stream-type.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/filtering/by-event-stream-type.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/filtering/by-tag.md b/Documentation/client-snippets/reducers/filtering/by-tag.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/filtering/by-tag.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/filtering/combine-filters.md b/Documentation/client-snippets/reducers/filtering/combine-filters.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/filtering/combine-filters.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/filtering/metadata-example.md b/Documentation/client-snippets/reducers/filtering/metadata-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/filtering/metadata-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/filtering/multiple-tags.md b/Documentation/client-snippets/reducers/filtering/multiple-tags.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/filtering/multiple-tags.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/filtering/tag-vs-filter.md b/Documentation/client-snippets/reducers/filtering/tag-vs-filter.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/filtering/tag-vs-filter.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/getting-started/async-signatures.md b/Documentation/client-snippets/reducers/getting-started/async-signatures.md new file mode 100644 index 0000000..bc99565 --- /dev/null +++ b/Documentation/client-snippets/reducers/getting-started/async-signatures.md @@ -0,0 +1,23 @@ +```typescript +import { EventContext, eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class ReducersAsyncSignaturesOrderPlaced { + constructor(readonly orderId: string) {} +} + +class ReducersAsyncSignaturesOrderSummary { + orderId = ''; +} + +@reducer('', undefined, ReducersAsyncSignaturesOrderSummary) +class ReducersAsyncSignaturesOrderSummaryReducer { + // Async without context + async reducersAsyncSignaturesOrderPlaced( + event: ReducersAsyncSignaturesOrderPlaced, + current: ReducersAsyncSignaturesOrderSummary | undefined + ): Promise { + return { orderId: event.orderId }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/getting-started/sync-signatures.md b/Documentation/client-snippets/reducers/getting-started/sync-signatures.md new file mode 100644 index 0000000..f836b53 --- /dev/null +++ b/Documentation/client-snippets/reducers/getting-started/sync-signatures.md @@ -0,0 +1,25 @@ +```typescript +import { EventContext, eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class ReducersSyncSignaturesOrderPlaced { + constructor(readonly orderId: string) {} +} + +class ReducersSyncSignaturesOrderSummary { + orderId = ''; + lastUpdated = new Date(); +} + +@reducer('', undefined, ReducersSyncSignaturesOrderSummary) +class ReducersSyncSignaturesOrderSummaryReducer { + // Synchronous, with context + reducersSyncSignaturesOrderPlaced( + event: ReducersSyncSignaturesOrderPlaced, + current: ReducersSyncSignaturesOrderSummary | undefined, + context: EventContext + ): ReducersSyncSignaturesOrderSummary { + return { orderId: event.orderId, lastUpdated: context.occurred }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/passive-reducers/development-testing.md b/Documentation/client-snippets/reducers/passive-reducers/development-testing.md new file mode 100644 index 0000000..3269b55 --- /dev/null +++ b/Documentation/client-snippets/reducers/passive-reducers/development-testing.md @@ -0,0 +1,12 @@ +```typescript +import { reducer } from '@cratis/chronicle'; + +class PassiveReducersExperimentalMetrics { + sampleCount = 0; +} + +// Kept passive while experimental - flip isActive to true once the metric is trusted +@reducer('', undefined, PassiveReducersExperimentalMetrics, false) +class PassiveReducersExperimentalMetricsReducer { +} +``` diff --git a/Documentation/client-snippets/reducers/passive-reducers/historical-snapshots.md b/Documentation/client-snippets/reducers/passive-reducers/historical-snapshots.md new file mode 100644 index 0000000..c0c98f4 --- /dev/null +++ b/Documentation/client-snippets/reducers/passive-reducers/historical-snapshots.md @@ -0,0 +1,16 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class PassiveReducersAccountBalance { + balance = 0; +} + +class PassiveReducersHistoricalBalanceService { + constructor(private readonly store: IEventStore) {} + + // Passive reducer computes state on-demand from historical events + getBalanceAtDate(accountId: string): Promise { + return this.store.readModels.getInstanceById(PassiveReducersAccountBalance, accountId); + } +} +``` diff --git a/Documentation/client-snippets/reducers/passive-reducers/on-demand-reports.md b/Documentation/client-snippets/reducers/passive-reducers/on-demand-reports.md new file mode 100644 index 0000000..1cb895f --- /dev/null +++ b/Documentation/client-snippets/reducers/passive-reducers/on-demand-reports.md @@ -0,0 +1,36 @@ +```typescript +import { EventContext, eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class PassiveReducersPaymentReceived { + constructor(readonly category: string, readonly amount: number) {} +} + +class PassiveReducersMonthlyRevenueReport { + totalRevenue = 0; + revenueByCategory: Record = {}; + month = 0; + year = 0; +} + +@reducer('', undefined, PassiveReducersMonthlyRevenueReport, false) +class PassiveReducersMonthlyRevenueReportReducer { + passiveReducersPaymentReceived( + event: PassiveReducersPaymentReceived, + current: PassiveReducersMonthlyRevenueReport | undefined, + context: EventContext + ): PassiveReducersMonthlyRevenueReport { + const revenue = current?.totalRevenue ?? 0; + const byCategory = { ...(current?.revenueByCategory ?? {}) }; + + byCategory[event.category] = (byCategory[event.category] ?? 0) + event.amount; + + return { + totalRevenue: revenue + event.amount, + revenueByCategory: byCategory, + month: context.occurred.getMonth() + 1, + year: context.occurred.getFullYear() + }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/passive-reducers/passive-attribute.md b/Documentation/client-snippets/reducers/passive-reducers/passive-attribute.md new file mode 100644 index 0000000..0184b61 --- /dev/null +++ b/Documentation/client-snippets/reducers/passive-reducers/passive-attribute.md @@ -0,0 +1,28 @@ +```typescript +import { EventContext, eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class PassiveReducersTransactionCompleted { + constructor(readonly amount: number) {} +} + +class PassiveReducersAdHocReport { + totalRevenue = 0; + transactionCount = 0; + generatedAt = new Date(); +} + +@reducer('', undefined, PassiveReducersAdHocReport, false) +class PassiveReducersAdHocReportReducer { + passiveReducersTransactionCompleted( + event: PassiveReducersTransactionCompleted, + current: PassiveReducersAdHocReport | undefined, + context: EventContext + ): PassiveReducersAdHocReport { + const revenue = current?.totalRevenue ?? 0; + const count = current?.transactionCount ?? 0; + + return { totalRevenue: revenue + event.amount, transactionCount: count + 1, generatedAt: context.occurred }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/passive-reducers/reducer-attribute.md b/Documentation/client-snippets/reducers/passive-reducers/reducer-attribute.md new file mode 100644 index 0000000..4047b5a --- /dev/null +++ b/Documentation/client-snippets/reducers/passive-reducers/reducer-attribute.md @@ -0,0 +1,29 @@ +```typescript +import { EventContext, eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class PassiveReducersDataRecorded { + constructor(readonly value: number) {} +} + +class PassiveReducersAnalytics { + recordCount = 0; + totalValue = 0; + lastUpdated = new Date(); +} + +// isActive: false — registered with the Kernel but does not automatically observe events +@reducer('', undefined, PassiveReducersAnalytics, false) +class PassiveReducersTemporaryAnalyticsReducer { + passiveReducersDataRecorded( + event: PassiveReducersDataRecorded, + current: PassiveReducersAnalytics | undefined, + context: EventContext + ): PassiveReducersAnalytics { + const count = current?.recordCount ?? 0; + const sum = current?.totalValue ?? 0; + + return { recordCount: count + 1, totalValue: sum + event.value, lastUpdated: context.occurred }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/passive-reducers/retrieving-state.md b/Documentation/client-snippets/reducers/passive-reducers/retrieving-state.md new file mode 100644 index 0000000..18e7dc8 --- /dev/null +++ b/Documentation/client-snippets/reducers/passive-reducers/retrieving-state.md @@ -0,0 +1,12 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class PassiveReducersReportingService { + constructor(private readonly store: IEventStore) {} + + // This triggers the passive reducer to compute state from events + generateReport(reportId: string): Promise { + return this.store.readModels.getInstanceById(PassiveReducersMonthlyRevenueReport, reportId); + } +} +``` diff --git a/Documentation/client-snippets/reducers/passive-reducers/switching-active-passive.md b/Documentation/client-snippets/reducers/passive-reducers/switching-active-passive.md new file mode 100644 index 0000000..926e6ca --- /dev/null +++ b/Documentation/client-snippets/reducers/passive-reducers/switching-active-passive.md @@ -0,0 +1,12 @@ +```typescript +import { reducer } from '@cratis/chronicle'; + +class PassiveReducersSwitchableReadModel { + value = 0; +} + +// Was active, now passive +@reducer('', undefined, PassiveReducersSwitchableReadModel, false) +class PassiveReducersSwitchableReducer { +} +``` diff --git a/Documentation/client-snippets/reducers/passive-reducers/temporary-analysis.md b/Documentation/client-snippets/reducers/passive-reducers/temporary-analysis.md new file mode 100644 index 0000000..356f186 --- /dev/null +++ b/Documentation/client-snippets/reducers/passive-reducers/temporary-analysis.md @@ -0,0 +1,13 @@ +```typescript +import { reducer } from '@cratis/chronicle'; + +class PassiveReducersCustomerBehaviorAnalysis { + uniqueCustomers = 0; + averageOrderValue = 0; + ordersByHour: Record = {}; +} + +@reducer('', undefined, PassiveReducersCustomerBehaviorAnalysis, false) +class PassiveReducersCustomerBehaviorAnalysisReducer { +} +``` diff --git a/Documentation/client-snippets/reducers/tagging-reducers/mixed-approach.md b/Documentation/client-snippets/reducers/tagging-reducers/mixed-approach.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/tagging-reducers/mixed-approach.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-multiple-attributes.md b/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-multiple-attributes.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-multiple-attributes.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-single-attribute.md b/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-single-attribute.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-single-attribute.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/tagging-reducers/single-tag.md b/Documentation/client-snippets/reducers/tagging-reducers/single-tag.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/tagging-reducers/single-tag.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reducers/tagging-reducers/tag-categories.md b/Documentation/client-snippets/reducers/tagging-reducers/tag-categories.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reducers/tagging-reducers/tag-categories.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 7de0bb0dbdfaae8bdea2e61eac9cee0fc345d7aa Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:24:09 +0200 Subject: [PATCH 16/39] Add TypeScript snippets for reactors docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../reactors/delivery-identity/basic.md | 3 ++ .../delivery-identity/with-once-only.md | 3 ++ .../reactors/event-processing/dependencies.md | 3 ++ .../event-processing/read-model-key.md | 3 ++ .../event-processing/supported-signatures.md | 17 +++++++++++ .../event-sequence/event-log-attribute.md | 18 ++++++++++++ .../event-sequence-attribute.md | 17 +++++++++++ .../automatic-routing.md | 3 ++ .../observer-level-event-store.md | 3 ++ .../filtering/by-event-source-type.md | 3 ++ .../filtering/by-event-stream-type.md | 3 ++ .../reactors/filtering/by-tag.md | 3 ++ .../reactors/filtering/combine-filters.md | 3 ++ .../reactors/filtering/metadata-example.md | 3 ++ .../reactors/filtering/multiple-tags.md | 3 ++ .../reactors/filtering/tag-vs-filter.md | 3 ++ .../reactors/once-only/basic.md | 3 ++ .../reactors/side-effects/custom-handler.md | 3 ++ .../side-effects/explicit-metadata.md | 25 +++++++++++++++++ .../reactors/side-effects/fan-out.md | 28 +++++++++++++++++++ .../side-effects/provide-event-source-id.md | 3 ++ .../side-effects/provide-event-stream-id.md | 3 ++ .../reactors/side-effects/provide-subject.md | 3 ++ .../reactors/side-effects/source-event.md | 3 ++ .../side-effects/stream-and-source-type.md | 3 ++ .../side-effects/with-concurrency-scopes.md | 3 ++ 26 files changed, 168 insertions(+) create mode 100644 Documentation/client-snippets/reactors/delivery-identity/basic.md create mode 100644 Documentation/client-snippets/reactors/delivery-identity/with-once-only.md create mode 100644 Documentation/client-snippets/reactors/event-processing/dependencies.md create mode 100644 Documentation/client-snippets/reactors/event-processing/read-model-key.md create mode 100644 Documentation/client-snippets/reactors/event-processing/supported-signatures.md create mode 100644 Documentation/client-snippets/reactors/event-sequence/event-log-attribute.md create mode 100644 Documentation/client-snippets/reactors/event-sequence/event-sequence-attribute.md create mode 100644 Documentation/client-snippets/reactors/external-event-store-subscriptions/automatic-routing.md create mode 100644 Documentation/client-snippets/reactors/external-event-store-subscriptions/observer-level-event-store.md create mode 100644 Documentation/client-snippets/reactors/filtering/by-event-source-type.md create mode 100644 Documentation/client-snippets/reactors/filtering/by-event-stream-type.md create mode 100644 Documentation/client-snippets/reactors/filtering/by-tag.md create mode 100644 Documentation/client-snippets/reactors/filtering/combine-filters.md create mode 100644 Documentation/client-snippets/reactors/filtering/metadata-example.md create mode 100644 Documentation/client-snippets/reactors/filtering/multiple-tags.md create mode 100644 Documentation/client-snippets/reactors/filtering/tag-vs-filter.md create mode 100644 Documentation/client-snippets/reactors/once-only/basic.md create mode 100644 Documentation/client-snippets/reactors/side-effects/custom-handler.md create mode 100644 Documentation/client-snippets/reactors/side-effects/explicit-metadata.md create mode 100644 Documentation/client-snippets/reactors/side-effects/fan-out.md create mode 100644 Documentation/client-snippets/reactors/side-effects/provide-event-source-id.md create mode 100644 Documentation/client-snippets/reactors/side-effects/provide-event-stream-id.md create mode 100644 Documentation/client-snippets/reactors/side-effects/provide-subject.md create mode 100644 Documentation/client-snippets/reactors/side-effects/source-event.md create mode 100644 Documentation/client-snippets/reactors/side-effects/stream-and-source-type.md create mode 100644 Documentation/client-snippets/reactors/side-effects/with-concurrency-scopes.md diff --git a/Documentation/client-snippets/reactors/delivery-identity/basic.md b/Documentation/client-snippets/reactors/delivery-identity/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/delivery-identity/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/delivery-identity/with-once-only.md b/Documentation/client-snippets/reactors/delivery-identity/with-once-only.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/delivery-identity/with-once-only.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/event-processing/dependencies.md b/Documentation/client-snippets/reactors/event-processing/dependencies.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/event-processing/dependencies.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/event-processing/read-model-key.md b/Documentation/client-snippets/reactors/event-processing/read-model-key.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/event-processing/read-model-key.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/event-processing/supported-signatures.md b/Documentation/client-snippets/reactors/event-processing/supported-signatures.md new file mode 100644 index 0000000..70f69bd --- /dev/null +++ b/Documentation/client-snippets/reactors/event-processing/supported-signatures.md @@ -0,0 +1,17 @@ +```typescript +import { EventContext } from '@cratis/chronicle'; + +interface ReactorHandlerSignatures { + methodName(event: TEvent): void; + methodName(event: TEvent, context: EventContext): void; + + methodNameAsync(event: TEvent): Promise; + methodNameAsync(event: TEvent, context: EventContext): Promise; + + methodNameReturningAsync(event: TEvent): Promise; + methodNameReturningAsync(event: TEvent, context: EventContext): Promise; + + methodNameReturning(event: TEvent): TResult; + methodNameReturning(event: TEvent, context: EventContext): TResult; +} +``` diff --git a/Documentation/client-snippets/reactors/event-sequence/event-log-attribute.md b/Documentation/client-snippets/reactors/event-sequence/event-log-attribute.md new file mode 100644 index 0000000..50ae50a --- /dev/null +++ b/Documentation/client-snippets/reactors/event-sequence/event-log-attribute.md @@ -0,0 +1,18 @@ +```typescript +import { EventContext, eventType, Guid, reactor } from '@cratis/chronicle'; + +@eventType() +class EventSequenceLogReactorOrderPlaced { + constructor(readonly orderId: Guid) {} +} + +// No eventSequenceId given - observes the default event log +@reactor() +class EventSequenceLocalAuditReactor { + async eventSequenceLogReactorOrderPlaced(event: EventSequenceLogReactorOrderPlaced, context: EventContext): Promise { + await this.writeAudit(event.orderId, context.occurred); + } + + private async writeAudit(orderId: Guid, occurred: Date): Promise {} +} +``` diff --git a/Documentation/client-snippets/reactors/event-sequence/event-sequence-attribute.md b/Documentation/client-snippets/reactors/event-sequence/event-sequence-attribute.md new file mode 100644 index 0000000..10bd0d5 --- /dev/null +++ b/Documentation/client-snippets/reactors/event-sequence/event-sequence-attribute.md @@ -0,0 +1,17 @@ +```typescript +import { EventContext, eventType, reactor } from '@cratis/chronicle'; + +@eventType() +class EventSequenceReactorShipmentDispatched { + constructor(readonly trackingNumber: string) {} +} + +@reactor('', 'fulfillment-events') +class EventSequenceShipmentReactor { + async eventSequenceReactorShipmentDispatched(event: EventSequenceReactorShipmentDispatched, context: EventContext): Promise { + await this.notifyCarrier(event.trackingNumber); + } + + private async notifyCarrier(trackingNumber: string): Promise {} +} +``` diff --git a/Documentation/client-snippets/reactors/external-event-store-subscriptions/automatic-routing.md b/Documentation/client-snippets/reactors/external-event-store-subscriptions/automatic-routing.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/external-event-store-subscriptions/automatic-routing.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/external-event-store-subscriptions/observer-level-event-store.md b/Documentation/client-snippets/reactors/external-event-store-subscriptions/observer-level-event-store.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/external-event-store-subscriptions/observer-level-event-store.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/filtering/by-event-source-type.md b/Documentation/client-snippets/reactors/filtering/by-event-source-type.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/filtering/by-event-source-type.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/filtering/by-event-stream-type.md b/Documentation/client-snippets/reactors/filtering/by-event-stream-type.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/filtering/by-event-stream-type.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/filtering/by-tag.md b/Documentation/client-snippets/reactors/filtering/by-tag.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/filtering/by-tag.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/filtering/combine-filters.md b/Documentation/client-snippets/reactors/filtering/combine-filters.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/filtering/combine-filters.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/filtering/metadata-example.md b/Documentation/client-snippets/reactors/filtering/metadata-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/filtering/metadata-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/filtering/multiple-tags.md b/Documentation/client-snippets/reactors/filtering/multiple-tags.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/filtering/multiple-tags.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/filtering/tag-vs-filter.md b/Documentation/client-snippets/reactors/filtering/tag-vs-filter.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/filtering/tag-vs-filter.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/once-only/basic.md b/Documentation/client-snippets/reactors/once-only/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/once-only/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/side-effects/custom-handler.md b/Documentation/client-snippets/reactors/side-effects/custom-handler.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/side-effects/custom-handler.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/side-effects/explicit-metadata.md b/Documentation/client-snippets/reactors/side-effects/explicit-metadata.md new file mode 100644 index 0000000..9b06c11 --- /dev/null +++ b/Documentation/client-snippets/reactors/side-effects/explicit-metadata.md @@ -0,0 +1,25 @@ +```typescript +import { EventContext, EventForEventSourceId, eventType, reactor } from '@cratis/chronicle'; + +@eventType() +class ExplicitMetadataBookReserved { + constructor(readonly isbn: string = '', readonly memberId: string = '') {} +} + +@eventType() +class ExplicitMetadataMemberActivityRecorded { + constructor(readonly isbn: string = '') {} +} + +@reactor() +class ExplicitMetadataReactor { + async explicitMetadataBookReserved(event: ExplicitMetadataBookReserved, context: EventContext): Promise { + return { + eventSourceId: event.memberId, + event: new ExplicitMetadataMemberActivityRecorded(event.isbn), + eventStreamType: 'members', + subject: event.memberId + }; + } +} +``` diff --git a/Documentation/client-snippets/reactors/side-effects/fan-out.md b/Documentation/client-snippets/reactors/side-effects/fan-out.md new file mode 100644 index 0000000..417444f --- /dev/null +++ b/Documentation/client-snippets/reactors/side-effects/fan-out.md @@ -0,0 +1,28 @@ +```typescript +import { EventContext, EventForEventSourceId, eventType, reactor } from '@cratis/chronicle'; + +@eventType() +class FanOutBookReserved { + constructor(readonly isbn: string = '', readonly memberId: string = '') {} +} + +@eventType() +class FanOutMemberActivityRecorded { + constructor(readonly isbn: string = '') {} +} + +@eventType() +class FanOutStockDecreased { + constructor(readonly isbn: string = '', readonly quantity: number = 0) {} +} + +@reactor() +class ReservationFanOutReactor { + async fanOutBookReserved(event: FanOutBookReserved, context: EventContext): Promise { + return [ + { eventSourceId: event.memberId, event: new FanOutMemberActivityRecorded(event.isbn) }, + { eventSourceId: event.isbn, event: new FanOutStockDecreased(event.isbn, 1) } + ]; + } +} +``` diff --git a/Documentation/client-snippets/reactors/side-effects/provide-event-source-id.md b/Documentation/client-snippets/reactors/side-effects/provide-event-source-id.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/side-effects/provide-event-source-id.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/side-effects/provide-event-stream-id.md b/Documentation/client-snippets/reactors/side-effects/provide-event-stream-id.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/side-effects/provide-event-stream-id.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/side-effects/provide-subject.md b/Documentation/client-snippets/reactors/side-effects/provide-subject.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/side-effects/provide-subject.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/side-effects/source-event.md b/Documentation/client-snippets/reactors/side-effects/source-event.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/side-effects/source-event.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/side-effects/stream-and-source-type.md b/Documentation/client-snippets/reactors/side-effects/stream-and-source-type.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/side-effects/stream-and-source-type.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/reactors/side-effects/with-concurrency-scopes.md b/Documentation/client-snippets/reactors/side-effects/with-concurrency-scopes.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/reactors/side-effects/with-concurrency-scopes.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From afcba56f99fa1a62117b9b6c4efc3c18bf8f8cc9 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:25:32 +0200 Subject: [PATCH 17/39] Add TypeScript snippets for read-models docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../empty-child-collections/declaring.md | 3 ++ .../named-constants.md | 3 ++ .../observing-in-service.md | 13 +++++++++ .../materialized-pagination/paged-endpoint.md | 3 ++ .../reacting-to-changes/collection.md | 3 ++ .../reacting-to-changes/dependencies.md | 3 ++ .../reacting-to-changes/materialized.md | 3 ++ .../reacting-to-changes/reactor.md | 3 ++ .../reacting-to-changes/side-effects.md | 3 ++ .../read-models/releasing-pii/collection.md | 11 ++++++++ .../read-models/releasing-pii/read-model.md | 28 +++++++++++++++++++ .../releasing-pii/single-instance.md | 11 ++++++++ .../read-models/releasing-pii/watch.md | 18 ++++++++++++ 13 files changed, 105 insertions(+) create mode 100644 Documentation/client-snippets/read-models/empty-child-collections/declaring.md create mode 100644 Documentation/client-snippets/read-models/materialized-pagination/named-constants.md create mode 100644 Documentation/client-snippets/read-models/materialized-pagination/observing-in-service.md create mode 100644 Documentation/client-snippets/read-models/materialized-pagination/paged-endpoint.md create mode 100644 Documentation/client-snippets/read-models/reacting-to-changes/collection.md create mode 100644 Documentation/client-snippets/read-models/reacting-to-changes/dependencies.md create mode 100644 Documentation/client-snippets/read-models/reacting-to-changes/materialized.md create mode 100644 Documentation/client-snippets/read-models/reacting-to-changes/reactor.md create mode 100644 Documentation/client-snippets/read-models/reacting-to-changes/side-effects.md create mode 100644 Documentation/client-snippets/read-models/releasing-pii/collection.md create mode 100644 Documentation/client-snippets/read-models/releasing-pii/read-model.md create mode 100644 Documentation/client-snippets/read-models/releasing-pii/single-instance.md create mode 100644 Documentation/client-snippets/read-models/releasing-pii/watch.md diff --git a/Documentation/client-snippets/read-models/empty-child-collections/declaring.md b/Documentation/client-snippets/read-models/empty-child-collections/declaring.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/read-models/empty-child-collections/declaring.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/read-models/materialized-pagination/named-constants.md b/Documentation/client-snippets/read-models/materialized-pagination/named-constants.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/read-models/materialized-pagination/named-constants.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/read-models/materialized-pagination/observing-in-service.md b/Documentation/client-snippets/read-models/materialized-pagination/observing-in-service.md new file mode 100644 index 0000000..08fda69 --- /dev/null +++ b/Documentation/client-snippets/read-models/materialized-pagination/observing-in-service.md @@ -0,0 +1,13 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class MaterializedPaginationProductDashboard { + constructor(private readonly store: IEventStore) {} + + async start(updateView: (products: MaterializedPaginationProduct[]) => void): Promise { + for await (const products of this.store.readModels.materialized.observeInstances(MaterializedPaginationProduct, 0, 100)) { + updateView(products); + } + } +} +``` diff --git a/Documentation/client-snippets/read-models/materialized-pagination/paged-endpoint.md b/Documentation/client-snippets/read-models/materialized-pagination/paged-endpoint.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/read-models/materialized-pagination/paged-endpoint.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/read-models/reacting-to-changes/collection.md b/Documentation/client-snippets/read-models/reacting-to-changes/collection.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/read-models/reacting-to-changes/collection.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/read-models/reacting-to-changes/dependencies.md b/Documentation/client-snippets/read-models/reacting-to-changes/dependencies.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/read-models/reacting-to-changes/dependencies.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/read-models/reacting-to-changes/materialized.md b/Documentation/client-snippets/read-models/reacting-to-changes/materialized.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/read-models/reacting-to-changes/materialized.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/read-models/reacting-to-changes/reactor.md b/Documentation/client-snippets/read-models/reacting-to-changes/reactor.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/read-models/reacting-to-changes/reactor.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/read-models/reacting-to-changes/side-effects.md b/Documentation/client-snippets/read-models/reacting-to-changes/side-effects.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/read-models/reacting-to-changes/side-effects.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/read-models/releasing-pii/collection.md b/Documentation/client-snippets/read-models/releasing-pii/collection.md new file mode 100644 index 0000000..ef08755 --- /dev/null +++ b/Documentation/client-snippets/read-models/releasing-pii/collection.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ReleasingPiiSupportTicketBatchService { + constructor(private readonly store: IEventStore) {} + + releaseAll(tickets: ReleasingPiiSupportTicket[]): Promise { + return this.store.readModels.releaseMany(ReleasingPiiSupportTicket, tickets); + } +} +``` diff --git a/Documentation/client-snippets/read-models/releasing-pii/read-model.md b/Documentation/client-snippets/read-models/releasing-pii/read-model.md new file mode 100644 index 0000000..c592308 --- /dev/null +++ b/Documentation/client-snippets/read-models/releasing-pii/read-model.md @@ -0,0 +1,28 @@ +```typescript +import { EventContext, eventType, pii, reducer } from '@cratis/chronicle'; + +@eventType() +class ReleasingPiiSupportTicketOpened { + constructor(readonly customerId: string, readonly requesterName: string) {} +} + +class ReleasingPiiSupportTicket { + // Read models are keyed by 'id' by convention - the same property release() uses as the subject. + id = ''; + customerId = ''; + + @pii('The name of the person who opened the ticket') + requesterName = ''; +} + +@reducer('', undefined, ReleasingPiiSupportTicket) +class ReleasingPiiSupportTicketReducer { + releasingPiiSupportTicketOpened( + event: ReleasingPiiSupportTicketOpened, + current: ReleasingPiiSupportTicket | undefined, + context: EventContext + ): ReleasingPiiSupportTicket { + return { id: context.eventSourceId, customerId: event.customerId, requesterName: event.requesterName }; + } +} +``` diff --git a/Documentation/client-snippets/read-models/releasing-pii/single-instance.md b/Documentation/client-snippets/read-models/releasing-pii/single-instance.md new file mode 100644 index 0000000..1a04df6 --- /dev/null +++ b/Documentation/client-snippets/read-models/releasing-pii/single-instance.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ReleasingPiiSupportTicketService { + constructor(private readonly store: IEventStore) {} + + release(ticket: ReleasingPiiSupportTicket): Promise { + return this.store.readModels.release(ReleasingPiiSupportTicket, ticket); + } +} +``` diff --git a/Documentation/client-snippets/read-models/releasing-pii/watch.md b/Documentation/client-snippets/read-models/releasing-pii/watch.md new file mode 100644 index 0000000..7a67479 --- /dev/null +++ b/Documentation/client-snippets/read-models/releasing-pii/watch.md @@ -0,0 +1,18 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ReleasingPiiSupportTicketWatcher { + constructor(private readonly store: IEventStore) {} + + async start(): Promise { + for await (const changeset of this.store.readModels.watch(ReleasingPiiSupportTicket)) { + if (changeset.removed) { + continue; + } + + const ticket = await this.store.readModels.release(ReleasingPiiSupportTicket, changeset.readModel); + console.log(`${changeset.key}: ${ticket.requesterName}`); + } + } +} +``` From 503c82c86e896377f29eb49d0f352edf0da278b1 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:30:13 +0200 Subject: [PATCH 18/39] Add TypeScript snippets for testing docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../testing/event-append-collection/multiple-events.md | 3 +++ .../testing/event-append-collection/scope-lifetime.md | 3 +++ .../event-append-collection/shipment-events-and-reactor.md | 3 +++ .../testing/event-append-collection/shipment-given-context.md | 3 +++ .../testing/event-append-collection/shipment-spec.md | 3 +++ .../testing/event-append-collection/single-event.md | 3 +++ .../event-append-collection/violation-events-and-reactor.md | 3 +++ .../testing/event-append-collection/violation-given-context.md | 3 +++ .../testing/event-append-collection/violation-spec.md | 3 +++ .../client-snippets/testing/events/assertions/concurrency.md | 3 +++ .../testing/events/assertions/constraint-e2e.md | 3 +++ .../testing/events/assertions/constraint-violation.md | 3 +++ .../client-snippets/testing/events/assertions/errors.md | 3 +++ .../client-snippets/testing/events/assertions/failure.md | 3 +++ .../client-snippets/testing/events/assertions/happy-path.md | 3 +++ .../testing/events/assertions/no-constraint-violation.md | 3 +++ .../event-sequence-assertions/appended-event-at-position.md | 3 +++ .../events/event-sequence-assertions/appended-event-by-type.md | 3 +++ .../event-sequence-assertions/appended-event-with-result.md | 3 +++ .../events/event-sequence-assertions/by-event-source.md | 3 +++ .../testing/events/event-sequence-assertions/full-example.md | 3 +++ .../testing/events/event-sequence-assertions/predicate.md | 3 +++ .../events/event-sequence-assertions/tail-sequence-number.md | 3 +++ .../events/event-sequence-assertions/validator-no-sequence.md | 3 +++ .../testing/events/event-sequence-assertions/validator.md | 3 +++ .../client-snippets/testing/events/scenario/append-many.md | 3 +++ Documentation/client-snippets/testing/events/scenario/basic.md | 3 +++ .../client-snippets/testing/events/scenario/full-example.md | 3 +++ .../testing/events/scenario/given-multiple-sources.md | 3 +++ Documentation/client-snippets/testing/events/scenario/given.md | 3 +++ .../client-snippets/testing/events/scenario/when-builder.md | 3 +++ Documentation/client-snippets/testing/index/example-spec.md | 3 +++ .../client-snippets/testing/reactors/pure-function.md | 3 +++ .../client-snippets/testing/reactors/scenario/basic.md | 3 +++ .../client-snippets/testing/reactors/scenario/email-example.md | 3 +++ .../testing/reactors/scenario/injecting-dependencies.md | 3 +++ .../testing/reactors/scenario/multiple-sources.md | 3 +++ .../testing/reactors/scenario/produced-side-effects.md | 3 +++ .../client-snippets/testing/reactors/scenario/read-model.md | 3 +++ .../client-snippets/testing/reactors/scenario/services.md | 3 +++ .../client-snippets/testing/read-models/scenario/basic.md | 3 +++ .../client-snippets/testing/read-models/scenario/children.md | 3 +++ .../testing/read-models/scenario/fluent-projection-example.md | 3 +++ .../testing/read-models/scenario/initial-state.md | 3 +++ .../testing/read-models/scenario/injecting-dependencies.md | 3 +++ .../testing/read-models/scenario/model-bound-example.md | 3 +++ .../testing/read-models/scenario/multiple-instances.md | 3 +++ .../testing/read-models/scenario/preseeding-instances.md | 3 +++ .../testing/read-models/scenario/reducer-example.md | 3 +++ .../testing/read-models/scenario/registered-artifacts.md | 3 +++ .../testing/read-models/scenario/strict-event-subscription.md | 3 +++ .../testing/read-models/scenario/strict-fidelity.md | 3 +++ .../testing/read-models/scenario/substitutions.md | 3 +++ 53 files changed, 159 insertions(+) create mode 100644 Documentation/client-snippets/testing/event-append-collection/multiple-events.md create mode 100644 Documentation/client-snippets/testing/event-append-collection/scope-lifetime.md create mode 100644 Documentation/client-snippets/testing/event-append-collection/shipment-events-and-reactor.md create mode 100644 Documentation/client-snippets/testing/event-append-collection/shipment-given-context.md create mode 100644 Documentation/client-snippets/testing/event-append-collection/shipment-spec.md create mode 100644 Documentation/client-snippets/testing/event-append-collection/single-event.md create mode 100644 Documentation/client-snippets/testing/event-append-collection/violation-events-and-reactor.md create mode 100644 Documentation/client-snippets/testing/event-append-collection/violation-given-context.md create mode 100644 Documentation/client-snippets/testing/event-append-collection/violation-spec.md create mode 100644 Documentation/client-snippets/testing/events/assertions/concurrency.md create mode 100644 Documentation/client-snippets/testing/events/assertions/constraint-e2e.md create mode 100644 Documentation/client-snippets/testing/events/assertions/constraint-violation.md create mode 100644 Documentation/client-snippets/testing/events/assertions/errors.md create mode 100644 Documentation/client-snippets/testing/events/assertions/failure.md create mode 100644 Documentation/client-snippets/testing/events/assertions/happy-path.md create mode 100644 Documentation/client-snippets/testing/events/assertions/no-constraint-violation.md create mode 100644 Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-at-position.md create mode 100644 Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-by-type.md create mode 100644 Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-with-result.md create mode 100644 Documentation/client-snippets/testing/events/event-sequence-assertions/by-event-source.md create mode 100644 Documentation/client-snippets/testing/events/event-sequence-assertions/full-example.md create mode 100644 Documentation/client-snippets/testing/events/event-sequence-assertions/predicate.md create mode 100644 Documentation/client-snippets/testing/events/event-sequence-assertions/tail-sequence-number.md create mode 100644 Documentation/client-snippets/testing/events/event-sequence-assertions/validator-no-sequence.md create mode 100644 Documentation/client-snippets/testing/events/event-sequence-assertions/validator.md create mode 100644 Documentation/client-snippets/testing/events/scenario/append-many.md create mode 100644 Documentation/client-snippets/testing/events/scenario/basic.md create mode 100644 Documentation/client-snippets/testing/events/scenario/full-example.md create mode 100644 Documentation/client-snippets/testing/events/scenario/given-multiple-sources.md create mode 100644 Documentation/client-snippets/testing/events/scenario/given.md create mode 100644 Documentation/client-snippets/testing/events/scenario/when-builder.md create mode 100644 Documentation/client-snippets/testing/index/example-spec.md create mode 100644 Documentation/client-snippets/testing/reactors/pure-function.md create mode 100644 Documentation/client-snippets/testing/reactors/scenario/basic.md create mode 100644 Documentation/client-snippets/testing/reactors/scenario/email-example.md create mode 100644 Documentation/client-snippets/testing/reactors/scenario/injecting-dependencies.md create mode 100644 Documentation/client-snippets/testing/reactors/scenario/multiple-sources.md create mode 100644 Documentation/client-snippets/testing/reactors/scenario/produced-side-effects.md create mode 100644 Documentation/client-snippets/testing/reactors/scenario/read-model.md create mode 100644 Documentation/client-snippets/testing/reactors/scenario/services.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/basic.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/children.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/fluent-projection-example.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/initial-state.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/injecting-dependencies.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/model-bound-example.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/multiple-instances.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/preseeding-instances.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/reducer-example.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/registered-artifacts.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/strict-event-subscription.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/strict-fidelity.md create mode 100644 Documentation/client-snippets/testing/read-models/scenario/substitutions.md diff --git a/Documentation/client-snippets/testing/event-append-collection/multiple-events.md b/Documentation/client-snippets/testing/event-append-collection/multiple-events.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/event-append-collection/multiple-events.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/event-append-collection/scope-lifetime.md b/Documentation/client-snippets/testing/event-append-collection/scope-lifetime.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/event-append-collection/scope-lifetime.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/event-append-collection/shipment-events-and-reactor.md b/Documentation/client-snippets/testing/event-append-collection/shipment-events-and-reactor.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/event-append-collection/shipment-events-and-reactor.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/event-append-collection/shipment-given-context.md b/Documentation/client-snippets/testing/event-append-collection/shipment-given-context.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/event-append-collection/shipment-given-context.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/event-append-collection/shipment-spec.md b/Documentation/client-snippets/testing/event-append-collection/shipment-spec.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/event-append-collection/shipment-spec.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/event-append-collection/single-event.md b/Documentation/client-snippets/testing/event-append-collection/single-event.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/event-append-collection/single-event.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/event-append-collection/violation-events-and-reactor.md b/Documentation/client-snippets/testing/event-append-collection/violation-events-and-reactor.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/event-append-collection/violation-events-and-reactor.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/event-append-collection/violation-given-context.md b/Documentation/client-snippets/testing/event-append-collection/violation-given-context.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/event-append-collection/violation-given-context.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/event-append-collection/violation-spec.md b/Documentation/client-snippets/testing/event-append-collection/violation-spec.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/event-append-collection/violation-spec.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/assertions/concurrency.md b/Documentation/client-snippets/testing/events/assertions/concurrency.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/assertions/concurrency.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/assertions/constraint-e2e.md b/Documentation/client-snippets/testing/events/assertions/constraint-e2e.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/assertions/constraint-e2e.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/assertions/constraint-violation.md b/Documentation/client-snippets/testing/events/assertions/constraint-violation.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/assertions/constraint-violation.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/assertions/errors.md b/Documentation/client-snippets/testing/events/assertions/errors.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/assertions/errors.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/assertions/failure.md b/Documentation/client-snippets/testing/events/assertions/failure.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/assertions/failure.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/assertions/happy-path.md b/Documentation/client-snippets/testing/events/assertions/happy-path.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/assertions/happy-path.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/assertions/no-constraint-violation.md b/Documentation/client-snippets/testing/events/assertions/no-constraint-violation.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/assertions/no-constraint-violation.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-at-position.md b/Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-at-position.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-at-position.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-by-type.md b/Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-by-type.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-by-type.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-with-result.md b/Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-with-result.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/event-sequence-assertions/appended-event-with-result.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/event-sequence-assertions/by-event-source.md b/Documentation/client-snippets/testing/events/event-sequence-assertions/by-event-source.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/event-sequence-assertions/by-event-source.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/event-sequence-assertions/full-example.md b/Documentation/client-snippets/testing/events/event-sequence-assertions/full-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/event-sequence-assertions/full-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/event-sequence-assertions/predicate.md b/Documentation/client-snippets/testing/events/event-sequence-assertions/predicate.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/event-sequence-assertions/predicate.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/event-sequence-assertions/tail-sequence-number.md b/Documentation/client-snippets/testing/events/event-sequence-assertions/tail-sequence-number.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/event-sequence-assertions/tail-sequence-number.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/event-sequence-assertions/validator-no-sequence.md b/Documentation/client-snippets/testing/events/event-sequence-assertions/validator-no-sequence.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/event-sequence-assertions/validator-no-sequence.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/event-sequence-assertions/validator.md b/Documentation/client-snippets/testing/events/event-sequence-assertions/validator.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/event-sequence-assertions/validator.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/scenario/append-many.md b/Documentation/client-snippets/testing/events/scenario/append-many.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/scenario/append-many.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/scenario/basic.md b/Documentation/client-snippets/testing/events/scenario/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/scenario/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/scenario/full-example.md b/Documentation/client-snippets/testing/events/scenario/full-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/scenario/full-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/scenario/given-multiple-sources.md b/Documentation/client-snippets/testing/events/scenario/given-multiple-sources.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/scenario/given-multiple-sources.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/scenario/given.md b/Documentation/client-snippets/testing/events/scenario/given.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/scenario/given.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/events/scenario/when-builder.md b/Documentation/client-snippets/testing/events/scenario/when-builder.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/events/scenario/when-builder.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/index/example-spec.md b/Documentation/client-snippets/testing/index/example-spec.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/index/example-spec.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/reactors/pure-function.md b/Documentation/client-snippets/testing/reactors/pure-function.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/reactors/pure-function.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/reactors/scenario/basic.md b/Documentation/client-snippets/testing/reactors/scenario/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/reactors/scenario/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/reactors/scenario/email-example.md b/Documentation/client-snippets/testing/reactors/scenario/email-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/reactors/scenario/email-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/reactors/scenario/injecting-dependencies.md b/Documentation/client-snippets/testing/reactors/scenario/injecting-dependencies.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/reactors/scenario/injecting-dependencies.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/reactors/scenario/multiple-sources.md b/Documentation/client-snippets/testing/reactors/scenario/multiple-sources.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/reactors/scenario/multiple-sources.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/reactors/scenario/produced-side-effects.md b/Documentation/client-snippets/testing/reactors/scenario/produced-side-effects.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/reactors/scenario/produced-side-effects.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/reactors/scenario/read-model.md b/Documentation/client-snippets/testing/reactors/scenario/read-model.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/reactors/scenario/read-model.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/reactors/scenario/services.md b/Documentation/client-snippets/testing/reactors/scenario/services.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/reactors/scenario/services.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/basic.md b/Documentation/client-snippets/testing/read-models/scenario/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/children.md b/Documentation/client-snippets/testing/read-models/scenario/children.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/children.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/fluent-projection-example.md b/Documentation/client-snippets/testing/read-models/scenario/fluent-projection-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/fluent-projection-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/initial-state.md b/Documentation/client-snippets/testing/read-models/scenario/initial-state.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/initial-state.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/injecting-dependencies.md b/Documentation/client-snippets/testing/read-models/scenario/injecting-dependencies.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/injecting-dependencies.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/model-bound-example.md b/Documentation/client-snippets/testing/read-models/scenario/model-bound-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/model-bound-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/multiple-instances.md b/Documentation/client-snippets/testing/read-models/scenario/multiple-instances.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/multiple-instances.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/preseeding-instances.md b/Documentation/client-snippets/testing/read-models/scenario/preseeding-instances.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/preseeding-instances.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/reducer-example.md b/Documentation/client-snippets/testing/read-models/scenario/reducer-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/reducer-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/registered-artifacts.md b/Documentation/client-snippets/testing/read-models/scenario/registered-artifacts.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/registered-artifacts.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/strict-event-subscription.md b/Documentation/client-snippets/testing/read-models/scenario/strict-event-subscription.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/strict-event-subscription.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/strict-fidelity.md b/Documentation/client-snippets/testing/read-models/scenario/strict-fidelity.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/strict-fidelity.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/testing/read-models/scenario/substitutions.md b/Documentation/client-snippets/testing/read-models/scenario/substitutions.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/testing/read-models/scenario/substitutions.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From d09e9fcb89d9c9f7dde43a7c056b494c64249f21 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:30:13 +0200 Subject: [PATCH 19/39] Add TypeScript snippets for constraints docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../unique-event-type/mutually-exclusive.md | 24 ++++++++++++++ .../releasing-several-events.md | 29 +++++++++++++++++ .../unique-event-type/releasing.md | 25 +++++++++++++++ .../violation-message-callback.md | 3 ++ .../unique/releasing-several-events.md | 32 +++++++++++++++++++ .../unique/violation-message-callback.md | 3 ++ .../unique/several-removal-events.md | 3 ++ .../model-bound/unique/strongly-typed.md | 3 ++ .../model-bound/unique/violation-message.md | 3 ++ 9 files changed, 125 insertions(+) create mode 100644 Documentation/client-snippets/constraints/declarative/unique-event-type/mutually-exclusive.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique-event-type/releasing-several-events.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique-event-type/releasing.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique-event-type/violation-message-callback.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique/releasing-several-events.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique/violation-message-callback.md create mode 100644 Documentation/client-snippets/constraints/model-bound/unique/several-removal-events.md create mode 100644 Documentation/client-snippets/constraints/model-bound/unique/strongly-typed.md create mode 100644 Documentation/client-snippets/constraints/model-bound/unique/violation-message.md diff --git a/Documentation/client-snippets/constraints/declarative/unique-event-type/mutually-exclusive.md b/Documentation/client-snippets/constraints/declarative/unique-event-type/mutually-exclusive.md new file mode 100644 index 0000000..f9a788d --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique-event-type/mutually-exclusive.md @@ -0,0 +1,24 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder, Guid } from '@cratis/chronicle'; + +@eventType() +class ConstraintsPersonAliasedTo { + constructor(readonly target: Guid) {} +} + +@eventType() +class ConstraintsPersonErased { +} + +@constraint() +class ConstraintsPersonTerminalOutcome implements IConstraint { + // Both declarations share one constraint name, so they become a single constraint: + // at most one event drawn from { ConstraintsPersonAliasedTo, ConstraintsPersonErased } + // per person. A person merged away can no longer be erased, and neither event can + // occur twice. + define(builder: IConstraintBuilder): void { + builder.uniqueFor(ConstraintsPersonAliasedTo, 'This person already has a terminal outcome.', 'PersonTerminal'); + builder.uniqueFor(ConstraintsPersonErased, 'This person already has a terminal outcome.', 'PersonTerminal'); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique-event-type/releasing-several-events.md b/Documentation/client-snippets/constraints/declarative/unique-event-type/releasing-several-events.md new file mode 100644 index 0000000..5682fbc --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique-event-type/releasing-several-events.md @@ -0,0 +1,29 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueEventTypeSeveralLoanCheckedOut { + constructor(readonly title: string) {} +} + +@eventType() +class ConstraintsUniqueEventTypeSeveralLoanReturned { +} + +@eventType() +class ConstraintsUniqueEventTypeSeveralLoanWrittenOff { +} + +@constraint() +class ConstraintsUniqueEventTypeSeveralOneOpenLoan implements IConstraint { + // A loan is open until it is returned or written off. Both end the cycle, so the + // borrower can take the next loan whichever way the previous one finished. + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .on(ConstraintsUniqueEventTypeSeveralLoanCheckedOut) + .removedWith(ConstraintsUniqueEventTypeSeveralLoanReturned) + .removedWith(ConstraintsUniqueEventTypeSeveralLoanWrittenOff)); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique-event-type/releasing.md b/Documentation/client-snippets/constraints/declarative/unique-event-type/releasing.md new file mode 100644 index 0000000..c805e3a --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique-event-type/releasing.md @@ -0,0 +1,25 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueEventTypeShiftStarted { + constructor(readonly location: string) {} +} + +@eventType() +class ConstraintsUniqueEventTypeShiftEnded { +} + +@constraint() +class ConstraintsUniqueEventTypeOneOpenShift implements IConstraint { + // At most one open shift per employee. Ending the shift releases the constraint, + // so the next shift is allowed - without it the constraint could only say + // "at most one, ever", and the employee's second shift would be refused forever. + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .on(ConstraintsUniqueEventTypeShiftStarted) + .removedWith(ConstraintsUniqueEventTypeShiftEnded)); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique-event-type/violation-message-callback.md b/Documentation/client-snippets/constraints/declarative/unique-event-type/violation-message-callback.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique-event-type/violation-message-callback.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique/releasing-several-events.md b/Documentation/client-snippets/constraints/declarative/unique/releasing-several-events.md new file mode 100644 index 0000000..da44527 --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique/releasing-several-events.md @@ -0,0 +1,32 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueSeveralInvitationSent { + constructor(readonly emailAddress: string) {} +} + +@eventType() +class ConstraintsUniqueSeveralInvitationAccepted { +} + +@eventType() +class ConstraintsUniqueSeveralInvitationRevoked { +} + +@eventType() +class ConstraintsUniqueSeveralInvitationExpired { +} + +@constraint() +class ConstraintsUniqueSeveralInvitedAddress implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .on(ConstraintsUniqueSeveralInvitationSent, e => e.emailAddress) + .removedWith(ConstraintsUniqueSeveralInvitationAccepted) + .removedWith(ConstraintsUniqueSeveralInvitationRevoked) + .removedWith(ConstraintsUniqueSeveralInvitationExpired)); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique/violation-message-callback.md b/Documentation/client-snippets/constraints/declarative/unique/violation-message-callback.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique/violation-message-callback.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/model-bound/unique/several-removal-events.md b/Documentation/client-snippets/constraints/model-bound/unique/several-removal-events.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/model-bound/unique/several-removal-events.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/model-bound/unique/strongly-typed.md b/Documentation/client-snippets/constraints/model-bound/unique/strongly-typed.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/model-bound/unique/strongly-typed.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/model-bound/unique/violation-message.md b/Documentation/client-snippets/constraints/model-bound/unique/violation-message.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/model-bound/unique/violation-message.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 28219cb2af4a56a153036a10867f66798b811272 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:30:14 +0200 Subject: [PATCH 20/39] Add TypeScript snippets for migrations docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../migrations/dotnet-client/combine.md | 27 +++++++++++ .../migrations/dotnet-client/default-value.md | 32 +++++++++++++ .../migrations/dotnet-client/generations.md | 16 +++++++ .../dotnet-client/map-values-one-direction.md | 3 ++ .../migrations/dotnet-client/map-values.md | 3 ++ .../migrations/dotnet-client/migrator.md | 17 +++++++ .../dotnet-client/multi-generation.md | 47 +++++++++++++++++++ .../migrations/dotnet-client/renamed-from.md | 26 ++++++++++ .../migrations/dotnet-client/split.md | 27 +++++++++++ .../migrations/validation/default-value.md | 26 ++++++++++ .../validation/enable-validation.md | 3 ++ 11 files changed, 227 insertions(+) create mode 100644 Documentation/client-snippets/migrations/dotnet-client/combine.md create mode 100644 Documentation/client-snippets/migrations/dotnet-client/default-value.md create mode 100644 Documentation/client-snippets/migrations/dotnet-client/generations.md create mode 100644 Documentation/client-snippets/migrations/dotnet-client/map-values-one-direction.md create mode 100644 Documentation/client-snippets/migrations/dotnet-client/map-values.md create mode 100644 Documentation/client-snippets/migrations/dotnet-client/migrator.md create mode 100644 Documentation/client-snippets/migrations/dotnet-client/multi-generation.md create mode 100644 Documentation/client-snippets/migrations/dotnet-client/renamed-from.md create mode 100644 Documentation/client-snippets/migrations/dotnet-client/split.md create mode 100644 Documentation/client-snippets/migrations/validation/default-value.md create mode 100644 Documentation/client-snippets/migrations/validation/enable-validation.md diff --git a/Documentation/client-snippets/migrations/dotnet-client/combine.md b/Documentation/client-snippets/migrations/dotnet-client/combine.md new file mode 100644 index 0000000..6285ad1 --- /dev/null +++ b/Documentation/client-snippets/migrations/dotnet-client/combine.md @@ -0,0 +1,27 @@ +```typescript +import { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle'; + +@eventType('dotnet-client-shipping-address-recorded', 2) +class MigrationsDotnetClientCombineShippingAddressRecorded { + constructor(readonly fullAddress: string) {} +} + +@eventType('dotnet-client-shipping-address-recorded', 1) +class MigrationsDotnetClientCombineShippingAddressRecordedV1 { + constructor(readonly street: string, readonly city: string) {} +} + +@eventTypeMigration(MigrationsDotnetClientCombineShippingAddressRecorded, MigrationsDotnetClientCombineShippingAddressRecordedV1) +class MigrationsDotnetClientCombineShippingAddressRecordedMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .combine('fullAddress', ' ', 'street', 'city')); + } + + downcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .split('street', 'fullAddress', ' ', 0) + .split('city', 'fullAddress', ' ', 1)); + } +} +``` diff --git a/Documentation/client-snippets/migrations/dotnet-client/default-value.md b/Documentation/client-snippets/migrations/dotnet-client/default-value.md new file mode 100644 index 0000000..64db5c1 --- /dev/null +++ b/Documentation/client-snippets/migrations/dotnet-client/default-value.md @@ -0,0 +1,32 @@ +```typescript +import { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle'; + +@eventType('dotnet-client-task-created', 2) +class MigrationsDotnetClientDefaultValueTaskCreated { + constructor( + readonly title: string, + readonly status: string, + readonly retryCount: number, + readonly enabled: boolean + ) {} +} + +@eventType('dotnet-client-task-created', 1) +class MigrationsDotnetClientDefaultValueTaskCreatedV1 { + constructor(readonly title: string) {} +} + +@eventTypeMigration(MigrationsDotnetClientDefaultValueTaskCreated, MigrationsDotnetClientDefaultValueTaskCreatedV1) +class MigrationsDotnetClientDefaultValueTaskCreatedMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .defaultValue('status', 'active') + .defaultValue('retryCount', 0) + .defaultValue('enabled', true)); + } + + downcast(builder: IEventMigrationBuilder): void { + // status, retryCount, and enabled did not exist in generation 1 — nothing to map back + } +} +``` diff --git a/Documentation/client-snippets/migrations/dotnet-client/generations.md b/Documentation/client-snippets/migrations/dotnet-client/generations.md new file mode 100644 index 0000000..3dfd566 --- /dev/null +++ b/Documentation/client-snippets/migrations/dotnet-client/generations.md @@ -0,0 +1,16 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +// Generation 2 (current) — Name has been split into FirstName and LastName +@eventType('dotnet-client-author-registered', 2) +class MigrationsDotnetClientAuthorRegistered { + constructor(readonly firstName: string, readonly lastName: string) {} +} + +// Generation 1 (original) — same id, generation 1, kept only so the migration below can +// upcast from it. It is not the "current" shape of the event any more. +@eventType('dotnet-client-author-registered', 1) +class MigrationsDotnetClientAuthorRegisteredV1 { + constructor(readonly name: string) {} +} +``` diff --git a/Documentation/client-snippets/migrations/dotnet-client/map-values-one-direction.md b/Documentation/client-snippets/migrations/dotnet-client/map-values-one-direction.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/migrations/dotnet-client/map-values-one-direction.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/migrations/dotnet-client/map-values.md b/Documentation/client-snippets/migrations/dotnet-client/map-values.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/migrations/dotnet-client/map-values.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/migrations/dotnet-client/migrator.md b/Documentation/client-snippets/migrations/dotnet-client/migrator.md new file mode 100644 index 0000000..2b1c24e --- /dev/null +++ b/Documentation/client-snippets/migrations/dotnet-client/migrator.md @@ -0,0 +1,17 @@ +```typescript +import { eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle'; + +@eventTypeMigration(MigrationsDotnetClientAuthorRegistered, MigrationsDotnetClientAuthorRegisteredV1) +class MigrationsDotnetClientAuthorRegisteredMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .split('firstName', 'name', ' ', 0) + .split('lastName', 'name', ' ', 1)); + } + + downcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .combine('name', ' ', 'firstName', 'lastName')); + } +} +``` diff --git a/Documentation/client-snippets/migrations/dotnet-client/multi-generation.md b/Documentation/client-snippets/migrations/dotnet-client/multi-generation.md new file mode 100644 index 0000000..6546839 --- /dev/null +++ b/Documentation/client-snippets/migrations/dotnet-client/multi-generation.md @@ -0,0 +1,47 @@ +```typescript +import { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle'; + +@eventType('dotnet-client-multi-gen-person-registered', 3) +class MigrationsDotnetClientMultiGenPersonRegistered { + constructor(readonly email: string, readonly firstName: string, readonly lastName: string) {} +} + +@eventType('dotnet-client-multi-gen-person-registered', 2) +class MigrationsDotnetClientMultiGenPersonRegisteredV2 { + constructor(readonly email: string, readonly name: string) {} +} + +@eventType('dotnet-client-multi-gen-person-registered', 1) +class MigrationsDotnetClientMultiGenPersonRegisteredV1 { + constructor(readonly emailAddress: string, readonly name: string) {} +} + +// Generation 1 → 2: rename emailAddress to email +@eventTypeMigration(MigrationsDotnetClientMultiGenPersonRegisteredV2, MigrationsDotnetClientMultiGenPersonRegisteredV1) +class MigrationsDotnetClientMultiGenPersonRegisteredV1ToV2 implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .renamedFrom('email', 'emailAddress')); + } + + downcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .renamedFrom('emailAddress', 'email')); + } +} + +// Generation 2 → 3: split name into firstName / lastName +@eventTypeMigration(MigrationsDotnetClientMultiGenPersonRegistered, MigrationsDotnetClientMultiGenPersonRegisteredV2) +class MigrationsDotnetClientMultiGenPersonRegisteredV2ToV3 implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .split('firstName', 'name', ' ', 0) + .split('lastName', 'name', ' ', 1)); + } + + downcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .combine('name', ' ', 'firstName', 'lastName')); + } +} +``` diff --git a/Documentation/client-snippets/migrations/dotnet-client/renamed-from.md b/Documentation/client-snippets/migrations/dotnet-client/renamed-from.md new file mode 100644 index 0000000..6b53525 --- /dev/null +++ b/Documentation/client-snippets/migrations/dotnet-client/renamed-from.md @@ -0,0 +1,26 @@ +```typescript +import { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle'; + +@eventType('dotnet-client-customer-registered', 2) +class MigrationsDotnetClientRenamedFromCustomerRegistered { + constructor(readonly email: string) {} +} + +@eventType('dotnet-client-customer-registered', 1) +class MigrationsDotnetClientRenamedFromCustomerRegisteredV1 { + constructor(readonly emailAddress: string) {} +} + +@eventTypeMigration(MigrationsDotnetClientRenamedFromCustomerRegistered, MigrationsDotnetClientRenamedFromCustomerRegisteredV1) +class MigrationsDotnetClientRenamedFromCustomerRegisteredMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .renamedFrom('email', 'emailAddress')); + } + + downcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .renamedFrom('emailAddress', 'email')); + } +} +``` diff --git a/Documentation/client-snippets/migrations/dotnet-client/split.md b/Documentation/client-snippets/migrations/dotnet-client/split.md new file mode 100644 index 0000000..0b97fb1 --- /dev/null +++ b/Documentation/client-snippets/migrations/dotnet-client/split.md @@ -0,0 +1,27 @@ +```typescript +import { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle'; + +@eventType('dotnet-client-person-registered', 2) +class MigrationsDotnetClientSplitPersonRegistered { + constructor(readonly firstName: string, readonly lastName: string) {} +} + +@eventType('dotnet-client-person-registered', 1) +class MigrationsDotnetClientSplitPersonRegisteredV1 { + constructor(readonly fullName: string) {} +} + +@eventTypeMigration(MigrationsDotnetClientSplitPersonRegistered, MigrationsDotnetClientSplitPersonRegisteredV1) +class MigrationsDotnetClientSplitPersonRegisteredMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .split('firstName', 'fullName', ' ', 0) + .split('lastName', 'fullName', ' ', 1)); + } + + downcast(builder: IEventMigrationBuilder): void { + builder.properties(propertyBuilder => propertyBuilder + .combine('fullName', ' ', 'firstName', 'lastName')); + } +} +``` diff --git a/Documentation/client-snippets/migrations/validation/default-value.md b/Documentation/client-snippets/migrations/validation/default-value.md new file mode 100644 index 0000000..558d3ca --- /dev/null +++ b/Documentation/client-snippets/migrations/validation/default-value.md @@ -0,0 +1,26 @@ +```typescript +import { eventType, eventTypeMigration, IEventMigrationBuilder, IEventTypeMigration } from '@cratis/chronicle'; + +@eventType('validation-author-registered', 2) +class MigrationsValidationAuthorRegistered { + constructor(readonly name: string, readonly status: string) {} +} + +@eventType('validation-author-registered', 1) +class MigrationsValidationAuthorRegisteredV1 { + constructor(readonly name: string) {} +} + +@eventTypeMigration(MigrationsValidationAuthorRegistered, MigrationsValidationAuthorRegisteredV1) +class MigrationsValidationAuthorRegisteredMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + // name is unchanged between generations — no operation needed for it + builder.properties(propertyBuilder => propertyBuilder + .defaultValue('status', 'active')); + } + + downcast(builder: IEventMigrationBuilder): void { + // status does not exist in generation 1 — no mapping needed + } +} +``` diff --git a/Documentation/client-snippets/migrations/validation/enable-validation.md b/Documentation/client-snippets/migrations/validation/enable-validation.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/migrations/validation/enable-validation.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 911d22786d684cf94b831761583fa57a54cfa60c Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:31:40 +0200 Subject: [PATCH 21/39] Add TypeScript snippets for subscriptions docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../startup-registration.md | 29 +++++++++++++++++ .../explicit-subscriptions/typical-pattern.md | 32 +++++++++++++++++++ .../implicit-subscriptions/consumer-setup.md | 3 ++ .../event-store-attribute.md | 3 ++ .../event-types-with-assembly-attribute.md | 3 ++ .../mixing-not-allowed.md | 3 ++ .../implicit-subscriptions/nuget-package.md | 3 ++ .../implicit-subscriptions/projection.md | 3 ++ .../separate-reactors.md | 3 ++ .../subscriptions/outbox-inbox/inbox-id.md | 3 ++ 10 files changed, 85 insertions(+) create mode 100644 Documentation/client-snippets/subscriptions/explicit-subscriptions/startup-registration.md create mode 100644 Documentation/client-snippets/subscriptions/explicit-subscriptions/typical-pattern.md create mode 100644 Documentation/client-snippets/subscriptions/implicit-subscriptions/consumer-setup.md create mode 100644 Documentation/client-snippets/subscriptions/implicit-subscriptions/event-store-attribute.md create mode 100644 Documentation/client-snippets/subscriptions/implicit-subscriptions/event-types-with-assembly-attribute.md create mode 100644 Documentation/client-snippets/subscriptions/implicit-subscriptions/mixing-not-allowed.md create mode 100644 Documentation/client-snippets/subscriptions/implicit-subscriptions/nuget-package.md create mode 100644 Documentation/client-snippets/subscriptions/implicit-subscriptions/projection.md create mode 100644 Documentation/client-snippets/subscriptions/implicit-subscriptions/separate-reactors.md create mode 100644 Documentation/client-snippets/subscriptions/outbox-inbox/inbox-id.md diff --git a/Documentation/client-snippets/subscriptions/explicit-subscriptions/startup-registration.md b/Documentation/client-snippets/subscriptions/explicit-subscriptions/startup-registration.md new file mode 100644 index 0000000..946a5fd --- /dev/null +++ b/Documentation/client-snippets/subscriptions/explicit-subscriptions/startup-registration.md @@ -0,0 +1,29 @@ +```typescript +import { ChronicleClient, ChronicleOptions, eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class SubscriptionsExplicitStartupShipmentDispatched { + constructor(readonly orderId: string, readonly trackingNumber: string) {} +} + +@eventType() +class SubscriptionsExplicitStartupStockAdjusted { + constructor(readonly sku: string, readonly delta: number) {} +} + +// Safe to call on every application startup +async function runSubscriptionsExplicitStartupRegistration(): Promise { + const client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000')); + const eventStore: IEventStore = await client.getEventStore('Quickstart'); + + await eventStore.subscriptions.subscribe( + 'orders-from-fulfillment', + 'fulfillment-service', + builder => builder.withEventType(SubscriptionsExplicitStartupShipmentDispatched)); + + await eventStore.subscriptions.subscribe( + 'inventory-from-warehouse', + 'warehouse-service', + builder => builder.withEventType(SubscriptionsExplicitStartupStockAdjusted)); +} +``` diff --git a/Documentation/client-snippets/subscriptions/explicit-subscriptions/typical-pattern.md b/Documentation/client-snippets/subscriptions/explicit-subscriptions/typical-pattern.md new file mode 100644 index 0000000..291d99c --- /dev/null +++ b/Documentation/client-snippets/subscriptions/explicit-subscriptions/typical-pattern.md @@ -0,0 +1,32 @@ +```typescript +import { eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class SubscriptionsExplicitTypicalShipmentDispatched { + constructor(readonly orderId: string, readonly trackingNumber: string) {} +} + +@eventType() +class SubscriptionsExplicitTypicalStockAdjusted { + constructor(readonly sku: string, readonly delta: number) {} +} + +@eventType() +class SubscriptionsExplicitTypicalStockReserved { + constructor(readonly sku: string, readonly quantity: number) {} +} + +async function registerSubscriptionsExplicitTypicalPattern(eventStore: IEventStore): Promise { + await eventStore.subscriptions.subscribe( + 'orders-from-fulfillment', + 'fulfillment-service', + builder => builder.withEventType(SubscriptionsExplicitTypicalShipmentDispatched)); + + await eventStore.subscriptions.subscribe( + 'inventory-updates', + 'warehouse-service', + builder => builder + .withEventType(SubscriptionsExplicitTypicalStockAdjusted) + .withEventType(SubscriptionsExplicitTypicalStockReserved)); +} +``` diff --git a/Documentation/client-snippets/subscriptions/implicit-subscriptions/consumer-setup.md b/Documentation/client-snippets/subscriptions/implicit-subscriptions/consumer-setup.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/implicit-subscriptions/consumer-setup.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/subscriptions/implicit-subscriptions/event-store-attribute.md b/Documentation/client-snippets/subscriptions/implicit-subscriptions/event-store-attribute.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/implicit-subscriptions/event-store-attribute.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/subscriptions/implicit-subscriptions/event-types-with-assembly-attribute.md b/Documentation/client-snippets/subscriptions/implicit-subscriptions/event-types-with-assembly-attribute.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/implicit-subscriptions/event-types-with-assembly-attribute.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/subscriptions/implicit-subscriptions/mixing-not-allowed.md b/Documentation/client-snippets/subscriptions/implicit-subscriptions/mixing-not-allowed.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/implicit-subscriptions/mixing-not-allowed.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/subscriptions/implicit-subscriptions/nuget-package.md b/Documentation/client-snippets/subscriptions/implicit-subscriptions/nuget-package.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/implicit-subscriptions/nuget-package.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/subscriptions/implicit-subscriptions/projection.md b/Documentation/client-snippets/subscriptions/implicit-subscriptions/projection.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/implicit-subscriptions/projection.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/subscriptions/implicit-subscriptions/separate-reactors.md b/Documentation/client-snippets/subscriptions/implicit-subscriptions/separate-reactors.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/implicit-subscriptions/separate-reactors.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/subscriptions/outbox-inbox/inbox-id.md b/Documentation/client-snippets/subscriptions/outbox-inbox/inbox-id.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/outbox-inbox/inbox-id.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From c4da5000be5ec8878e9bb60f3aef9a49ffa31114 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:33:19 +0200 Subject: [PATCH 22/39] Add TypeScript snippets for sinks docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../seeding-with-csharp/corrected-seed-set.md | 9 +++++ .../seeding-with-csharp/development-only.md | 13 +++++++ .../seeding-with-csharp/events.md | 23 +++++++++++ .../seeding-with-csharp/namespace-scoped.md | 38 +++++++++++++++++++ .../organize-by-feature.md | 17 +++++++++ .../seeding-with-csharp/seed-mixed-types.md | 14 +++++++ .../seed-multiple-same-type.md | 13 +++++++ .../seeding-with-csharp/seeder.md | 15 ++++++++ .../sinks/index/enable-sql-sink.md | 9 +++++ .../sinks/index/register-sql-storage.md | 3 ++ .../event-type-attribute.md | 3 ++ .../property-attribute.md | 3 ++ .../remove-constraint.md | 3 ++ 13 files changed, 163 insertions(+) create mode 100644 Documentation/client-snippets/event-seeding/seeding-with-csharp/corrected-seed-set.md create mode 100644 Documentation/client-snippets/event-seeding/seeding-with-csharp/development-only.md create mode 100644 Documentation/client-snippets/event-seeding/seeding-with-csharp/events.md create mode 100644 Documentation/client-snippets/event-seeding/seeding-with-csharp/namespace-scoped.md create mode 100644 Documentation/client-snippets/event-seeding/seeding-with-csharp/organize-by-feature.md create mode 100644 Documentation/client-snippets/event-seeding/seeding-with-csharp/seed-mixed-types.md create mode 100644 Documentation/client-snippets/event-seeding/seeding-with-csharp/seed-multiple-same-type.md create mode 100644 Documentation/client-snippets/event-seeding/seeding-with-csharp/seeder.md create mode 100644 Documentation/client-snippets/sinks/index/enable-sql-sink.md create mode 100644 Documentation/client-snippets/sinks/index/register-sql-storage.md create mode 100644 Documentation/client-snippets/understanding-constraints/event-type-attribute.md create mode 100644 Documentation/client-snippets/understanding-constraints/property-attribute.md create mode 100644 Documentation/client-snippets/understanding-constraints/remove-constraint.md diff --git a/Documentation/client-snippets/event-seeding/seeding-with-csharp/corrected-seed-set.md b/Documentation/client-snippets/event-seeding/seeding-with-csharp/corrected-seed-set.md new file mode 100644 index 0000000..aea3ff5 --- /dev/null +++ b/Documentation/client-snippets/event-seeding/seeding-with-csharp/corrected-seed-set.md @@ -0,0 +1,9 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +async function registerEvtSeedingCorrection(eventStore: IEventStore): Promise { + eventStore.seeding.for('user-123', [new EvtSeedingUserRegistered('john@example.com', 'John Doe')]); + + await eventStore.seeding.register(); +} +``` diff --git a/Documentation/client-snippets/event-seeding/seeding-with-csharp/development-only.md b/Documentation/client-snippets/event-seeding/seeding-with-csharp/development-only.md new file mode 100644 index 0000000..02aafb6 --- /dev/null +++ b/Documentation/client-snippets/event-seeding/seeding-with-csharp/development-only.md @@ -0,0 +1,13 @@ +```typescript +import { ICanSeedEvents, IEventSeedingBuilder, seeder } from '@cratis/chronicle'; + +// Only include this seeder's file in your development build/discovery patterns - +// TypeScript has no build-time equivalent of C#'s #if DEBUG, so keep it out of what +// ChronicleOptions.discoveryPatterns picks up for production. +@seeder() +class EvtSeedingDevelopmentSeeding implements ICanSeedEvents { + seed(builder: IEventSeedingBuilder): void { + builder.for('dev-user-1', [new EvtSeedingUserRegistered('dev@example.com', 'Dev User')]); + } +} +``` diff --git a/Documentation/client-snippets/event-seeding/seeding-with-csharp/events.md b/Documentation/client-snippets/event-seeding/seeding-with-csharp/events.md new file mode 100644 index 0000000..ed61ae1 --- /dev/null +++ b/Documentation/client-snippets/event-seeding/seeding-with-csharp/events.md @@ -0,0 +1,23 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class EvtSeedingUserRegistered { + constructor(readonly email: string, readonly displayName: string) {} +} + +@eventType() +class EvtSeedingEmailVerified { + constructor(readonly email: string) {} +} + +@eventType() +class EvtSeedingProfileUpdated { + constructor(readonly displayName: string) {} +} + +@eventType() +class EvtSeedingOrderPlaced { + constructor(readonly userId: string, readonly amount: number) {} +} +``` diff --git a/Documentation/client-snippets/event-seeding/seeding-with-csharp/namespace-scoped.md b/Documentation/client-snippets/event-seeding/seeding-with-csharp/namespace-scoped.md new file mode 100644 index 0000000..e34c4ec --- /dev/null +++ b/Documentation/client-snippets/event-seeding/seeding-with-csharp/namespace-scoped.md @@ -0,0 +1,38 @@ +```typescript +import { eventType, ICanSeedEvents, IEventSeedingBuilder, seeder } from '@cratis/chronicle'; + +@eventType() +class EvtSeedingProductCreated { + constructor(readonly name: string, readonly price: number) {} +} + +@eventType() +class EvtSeedingOrganizationCreated { + constructor(readonly name: string) {} +} + +@eventType() +class EvtSeedingBillingSetUp { + constructor(readonly billingEmail: string) {} +} + +@seeder() +class EvtSeedingTenantSeeding implements ICanSeedEvents { + seed(builder: IEventSeedingBuilder): void { + // Global seed data — applied to every namespace + builder.for('product-1', [new EvtSeedingProductCreated('Laptop', 1299.0)]); + + // Namespace-scoped seed data — applied only to the "acme" namespace + builder.forNamespace('acme') + .for('user-1', [new EvtSeedingUserRegistered('admin@acme.com', 'Acme Admin')]); + + // A second namespace with different seed data + builder.forNamespace('contoso') + .for('user-1', [new EvtSeedingUserRegistered('admin@contoso.com', 'Contoso Admin')]) + .forEventSource('org-1', [ + new EvtSeedingOrganizationCreated('Contoso'), + new EvtSeedingBillingSetUp('contoso@billing.com') + ]); + } +} +``` diff --git a/Documentation/client-snippets/event-seeding/seeding-with-csharp/organize-by-feature.md b/Documentation/client-snippets/event-seeding/seeding-with-csharp/organize-by-feature.md new file mode 100644 index 0000000..02c3c88 --- /dev/null +++ b/Documentation/client-snippets/event-seeding/seeding-with-csharp/organize-by-feature.md @@ -0,0 +1,17 @@ +```typescript +import { ICanSeedEvents, IEventSeedingBuilder, seeder } from '@cratis/chronicle'; + +@seeder() +class EvtSeedingUserFeatureSeeding implements ICanSeedEvents { + seed(builder: IEventSeedingBuilder): void { + builder.for('test-user-1', [new EvtSeedingUserRegistered('test1@example.com', 'Test User 1')]); + } +} + +@seeder() +class EvtSeedingOrderFeatureSeeding implements ICanSeedEvents { + seed(builder: IEventSeedingBuilder): void { + builder.for('test-order-1', [new EvtSeedingOrderPlaced('test-user-1', 100.0)]); + } +} +``` diff --git a/Documentation/client-snippets/event-seeding/seeding-with-csharp/seed-mixed-types.md b/Documentation/client-snippets/event-seeding/seeding-with-csharp/seed-mixed-types.md new file mode 100644 index 0000000..c9f35ff --- /dev/null +++ b/Documentation/client-snippets/event-seeding/seeding-with-csharp/seed-mixed-types.md @@ -0,0 +1,14 @@ +```typescript +import { ICanSeedEvents, IEventSeedingBuilder, seeder } from '@cratis/chronicle'; + +@seeder() +class EvtSeedingMixedTypesSeeding implements ICanSeedEvents { + seed(builder: IEventSeedingBuilder): void { + builder.forEventSource('user-123', [ + new EvtSeedingUserRegistered('john@example.com', 'John'), + new EvtSeedingEmailVerified('john@example.com'), + new EvtSeedingProfileUpdated('John Doe') + ]); + } +} +``` diff --git a/Documentation/client-snippets/event-seeding/seeding-with-csharp/seed-multiple-same-type.md b/Documentation/client-snippets/event-seeding/seeding-with-csharp/seed-multiple-same-type.md new file mode 100644 index 0000000..d657d03 --- /dev/null +++ b/Documentation/client-snippets/event-seeding/seeding-with-csharp/seed-multiple-same-type.md @@ -0,0 +1,13 @@ +```typescript +import { ICanSeedEvents, IEventSeedingBuilder, seeder } from '@cratis/chronicle'; + +@seeder() +class EvtSeedingMultipleSameTypeSeeding implements ICanSeedEvents { + seed(builder: IEventSeedingBuilder): void { + builder.for('user-123', [ + new EvtSeedingUserRegistered('john@example.com', 'John'), + new EvtSeedingUserRegistered('jane@example.com', 'Jane') + ]); + } +} +``` diff --git a/Documentation/client-snippets/event-seeding/seeding-with-csharp/seeder.md b/Documentation/client-snippets/event-seeding/seeding-with-csharp/seeder.md new file mode 100644 index 0000000..9aae063 --- /dev/null +++ b/Documentation/client-snippets/event-seeding/seeding-with-csharp/seeder.md @@ -0,0 +1,15 @@ +```typescript +import { ICanSeedEvents, IEventSeedingBuilder, seeder } from '@cratis/chronicle'; + +@seeder() +class EvtSeedingUserSeeding implements ICanSeedEvents { + seed(builder: IEventSeedingBuilder): void { + builder + .for('user-123', [new EvtSeedingUserRegistered('john@example.com', 'John')]) + .forEventSource('user-456', [ + new EvtSeedingUserRegistered('jane@example.com', 'Jane'), + new EvtSeedingEmailVerified('jane@example.com') + ]); + } +} +``` diff --git a/Documentation/client-snippets/sinks/index/enable-sql-sink.md b/Documentation/client-snippets/sinks/index/enable-sql-sink.md new file mode 100644 index 0000000..0e65ac4 --- /dev/null +++ b/Documentation/client-snippets/sinks/index/enable-sql-sink.md @@ -0,0 +1,9 @@ +```typescript +import { ChronicleOptions, WellKnownSinks } from '@cratis/chronicle'; + +function createSinksSqlOptions(): ChronicleOptions { + return ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { + defaultSinkTypeId: WellKnownSinks.SQL + }); +} +``` diff --git a/Documentation/client-snippets/sinks/index/register-sql-storage.md b/Documentation/client-snippets/sinks/index/register-sql-storage.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/sinks/index/register-sql-storage.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/understanding-constraints/event-type-attribute.md b/Documentation/client-snippets/understanding-constraints/event-type-attribute.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/understanding-constraints/event-type-attribute.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/understanding-constraints/property-attribute.md b/Documentation/client-snippets/understanding-constraints/property-attribute.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/understanding-constraints/property-attribute.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/understanding-constraints/remove-constraint.md b/Documentation/client-snippets/understanding-constraints/remove-constraint.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/understanding-constraints/remove-constraint.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 3d4ef2de56672c05dd8454078f7a690130f8b296 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:34:25 +0200 Subject: [PATCH 23/39] Add TypeScript snippets for concepts docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../correlation-identity-causation/renaming-an-identity.md | 7 +++++++ .../designing-read-models/constructor-may-not-run.md | 3 +++ .../concepts/event-type-migrations/map-values.md | 3 +++ .../concepts/geospatial/events-and-read-models.md | 3 +++ .../client-snippets/concepts/geospatial/projection.md | 3 +++ Documentation/client-snippets/concepts/geospatial/types.md | 3 +++ .../concepts/subject/implicit-with-attribute.md | 3 +++ .../concepts/tagging-reactors/mixed-approach.md | 3 +++ .../tagging-reactors/multiple-tags-multiple-attributes.md | 3 +++ .../tagging-reactors/multiple-tags-single-attribute.md | 3 +++ .../concepts/tagging-reactors/single-tag.md | 3 +++ .../concepts/tagging-reactors/tag-categories.md | 3 +++ .../concepts/tagging/by-communication-channel.md | 3 +++ .../client-snippets/concepts/tagging/by-domain.md | 3 +++ .../concepts/tagging/by-integration-type.md | 3 +++ .../client-snippets/concepts/tagging/by-purpose.md | 3 +++ .../client-snippets/concepts/tagging/by-stakeholder.md | 3 +++ .../concepts/tagging/dynamic-tags-patterns.md | 3 +++ .../concepts/tagging/observer-mixed-approach.md | 3 +++ .../tagging/observer-multiple-tags-multiple-attributes.md | 3 +++ .../tagging/observer-multiple-tags-single-attribute.md | 3 +++ .../concepts/tagging/observer-single-tag.md | 3 +++ .../client-snippets/concepts/tagging/read-model-tags.md | 3 +++ .../client-snippets/concepts/tagging/static-event-tags.md | 3 +++ .../concepts/tagging/tags-in-event-context.md | 3 +++ 25 files changed, 79 insertions(+) create mode 100644 Documentation/client-snippets/concepts/correlation-identity-causation/renaming-an-identity.md create mode 100644 Documentation/client-snippets/concepts/designing-read-models/constructor-may-not-run.md create mode 100644 Documentation/client-snippets/concepts/event-type-migrations/map-values.md create mode 100644 Documentation/client-snippets/concepts/geospatial/events-and-read-models.md create mode 100644 Documentation/client-snippets/concepts/geospatial/projection.md create mode 100644 Documentation/client-snippets/concepts/geospatial/types.md create mode 100644 Documentation/client-snippets/concepts/subject/implicit-with-attribute.md create mode 100644 Documentation/client-snippets/concepts/tagging-reactors/mixed-approach.md create mode 100644 Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-multiple-attributes.md create mode 100644 Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-single-attribute.md create mode 100644 Documentation/client-snippets/concepts/tagging-reactors/single-tag.md create mode 100644 Documentation/client-snippets/concepts/tagging-reactors/tag-categories.md create mode 100644 Documentation/client-snippets/concepts/tagging/by-communication-channel.md create mode 100644 Documentation/client-snippets/concepts/tagging/by-domain.md create mode 100644 Documentation/client-snippets/concepts/tagging/by-integration-type.md create mode 100644 Documentation/client-snippets/concepts/tagging/by-purpose.md create mode 100644 Documentation/client-snippets/concepts/tagging/by-stakeholder.md create mode 100644 Documentation/client-snippets/concepts/tagging/dynamic-tags-patterns.md create mode 100644 Documentation/client-snippets/concepts/tagging/observer-mixed-approach.md create mode 100644 Documentation/client-snippets/concepts/tagging/observer-multiple-tags-multiple-attributes.md create mode 100644 Documentation/client-snippets/concepts/tagging/observer-multiple-tags-single-attribute.md create mode 100644 Documentation/client-snippets/concepts/tagging/observer-single-tag.md create mode 100644 Documentation/client-snippets/concepts/tagging/read-model-tags.md create mode 100644 Documentation/client-snippets/concepts/tagging/static-event-tags.md create mode 100644 Documentation/client-snippets/concepts/tagging/tags-in-event-context.md diff --git a/Documentation/client-snippets/concepts/correlation-identity-causation/renaming-an-identity.md b/Documentation/client-snippets/concepts/correlation-identity-causation/renaming-an-identity.md new file mode 100644 index 0000000..0d8dac2 --- /dev/null +++ b/Documentation/client-snippets/concepts/correlation-identity-causation/renaming-an-identity.md @@ -0,0 +1,7 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +async function renameAnIdentity(eventStore: IEventStore): Promise { + await eventStore.identities.rename('subject-42', 'Jane Austen'); +} +``` diff --git a/Documentation/client-snippets/concepts/designing-read-models/constructor-may-not-run.md b/Documentation/client-snippets/concepts/designing-read-models/constructor-may-not-run.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/designing-read-models/constructor-may-not-run.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/event-type-migrations/map-values.md b/Documentation/client-snippets/concepts/event-type-migrations/map-values.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/event-type-migrations/map-values.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/geospatial/events-and-read-models.md b/Documentation/client-snippets/concepts/geospatial/events-and-read-models.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/geospatial/events-and-read-models.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/geospatial/projection.md b/Documentation/client-snippets/concepts/geospatial/projection.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/geospatial/projection.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/geospatial/types.md b/Documentation/client-snippets/concepts/geospatial/types.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/geospatial/types.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/subject/implicit-with-attribute.md b/Documentation/client-snippets/concepts/subject/implicit-with-attribute.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/subject/implicit-with-attribute.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging-reactors/mixed-approach.md b/Documentation/client-snippets/concepts/tagging-reactors/mixed-approach.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging-reactors/mixed-approach.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-multiple-attributes.md b/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-multiple-attributes.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-multiple-attributes.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-single-attribute.md b/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-single-attribute.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-single-attribute.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging-reactors/single-tag.md b/Documentation/client-snippets/concepts/tagging-reactors/single-tag.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging-reactors/single-tag.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging-reactors/tag-categories.md b/Documentation/client-snippets/concepts/tagging-reactors/tag-categories.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging-reactors/tag-categories.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/by-communication-channel.md b/Documentation/client-snippets/concepts/tagging/by-communication-channel.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/by-communication-channel.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/by-domain.md b/Documentation/client-snippets/concepts/tagging/by-domain.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/by-domain.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/by-integration-type.md b/Documentation/client-snippets/concepts/tagging/by-integration-type.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/by-integration-type.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/by-purpose.md b/Documentation/client-snippets/concepts/tagging/by-purpose.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/by-purpose.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/by-stakeholder.md b/Documentation/client-snippets/concepts/tagging/by-stakeholder.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/by-stakeholder.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/dynamic-tags-patterns.md b/Documentation/client-snippets/concepts/tagging/dynamic-tags-patterns.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/dynamic-tags-patterns.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/observer-mixed-approach.md b/Documentation/client-snippets/concepts/tagging/observer-mixed-approach.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/observer-mixed-approach.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-multiple-attributes.md b/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-multiple-attributes.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-multiple-attributes.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-single-attribute.md b/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-single-attribute.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-single-attribute.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/observer-single-tag.md b/Documentation/client-snippets/concepts/tagging/observer-single-tag.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/observer-single-tag.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/read-model-tags.md b/Documentation/client-snippets/concepts/tagging/read-model-tags.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/read-model-tags.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/static-event-tags.md b/Documentation/client-snippets/concepts/tagging/static-event-tags.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/static-event-tags.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/tags-in-event-context.md b/Documentation/client-snippets/concepts/tagging/tags-in-event-context.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/tags-in-event-context.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 0ebbf7c00567c1026b6afef8c3702a928383de88 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:37:15 +0200 Subject: [PATCH 24/39] Add TypeScript snippets for declarative children projections Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../projections/declarative/children/basic.md | 50 +++++++++++++++++++ .../children/context-parent-key.md | 36 +++++++++++++ .../children/default-parent-key-append.md | 11 ++++ .../children/default-parent-key.md | 35 +++++++++++++ .../children/event-parent-key-append.md | 11 ++++ .../declarative/children/event-parent-key.md | 36 +++++++++++++ .../declarative/children/events.md | 23 +++++++++ .../children/multiple-collections.md | 50 +++++++++++++++++++ .../declarative/children/read-model.md | 12 +++++ .../declarative/children/removing.md | 42 ++++++++++++++++ 10 files changed, 306 insertions(+) create mode 100644 Documentation/client-snippets/projections/declarative/children/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/children/context-parent-key.md create mode 100644 Documentation/client-snippets/projections/declarative/children/default-parent-key-append.md create mode 100644 Documentation/client-snippets/projections/declarative/children/default-parent-key.md create mode 100644 Documentation/client-snippets/projections/declarative/children/event-parent-key-append.md create mode 100644 Documentation/client-snippets/projections/declarative/children/event-parent-key.md create mode 100644 Documentation/client-snippets/projections/declarative/children/events.md create mode 100644 Documentation/client-snippets/projections/declarative/children/multiple-collections.md create mode 100644 Documentation/client-snippets/projections/declarative/children/read-model.md create mode 100644 Documentation/client-snippets/projections/declarative/children/removing.md diff --git a/Documentation/client-snippets/projections/declarative/children/basic.md b/Documentation/client-snippets/projections/declarative/children/basic.md new file mode 100644 index 0000000..963138c --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/children/basic.md @@ -0,0 +1,50 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class GroupCreatedForChildren { + constructor(readonly name: string, readonly description: string) {} +} + +@eventType() +class UserAddedToGroupForChildren { + constructor(readonly userId: string, readonly role: string) {} +} + +@eventType() +class UserRoleChangedForChildren { + constructor(readonly userId: string, readonly role: string) {} +} + +@eventType() +class UserRemovedFromGroupForChildren { + constructor(readonly userId: string) {} +} + +class GroupMemberForChildren { + userId = ''; + role = ''; +} + +class GroupForChildren { + name = ''; + description = ''; + members: GroupMemberForChildren[] = []; +} + +@projection() +class GroupProjectionForChildren implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(GroupCreatedForChildren) + .children(m => m.members, children => children + .identifiedBy(m => m.userId) + .from(UserAddedToGroupForChildren, b => b + .usingKey(e => e.userId)) + .from(UserRoleChangedForChildren, b => b + .usingKey(e => e.userId)) + .removedWith(UserRemovedFromGroupForChildren, b => b + .usingKey(e => e.userId))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/children/context-parent-key.md b/Documentation/client-snippets/projections/declarative/children/context-parent-key.md new file mode 100644 index 0000000..f81a4b5 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/children/context-parent-key.md @@ -0,0 +1,36 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class GroupCreatedWithContextParentKey { + constructor(readonly name: string) {} +} + +@eventType() +class UserAddedWithContextParentKey { + constructor(readonly userId: string, readonly role: string) {} +} + +class GroupMemberWithContextParentKey { + userId = ''; + role = ''; +} + +class GroupWithContextParentKey { + name = ''; + members: GroupMemberWithContextParentKey[] = []; +} + +@projection() +class GroupWithContextParentKeyProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(GroupCreatedWithContextParentKey) + .children(m => m.members, children => children + .identifiedBy(m => m.userId) + .from(UserAddedWithContextParentKey, b => b + .usingParentKeyFromContext('eventSourceId') + .usingKey(e => e.userId))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/children/default-parent-key-append.md b/Documentation/client-snippets/projections/declarative/children/default-parent-key-append.md new file mode 100644 index 0000000..705987e --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/children/default-parent-key-append.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class GroupMembershipWithDefaultParentKey { + constructor(private readonly eventStore: IEventStore) {} + + addUserToGroup(groupId: string, userId: string, role: string): Promise { + return this.eventStore.eventLog.append(groupId, new UserAddedWithDefaultParentKey(userId, role)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/children/default-parent-key.md b/Documentation/client-snippets/projections/declarative/children/default-parent-key.md new file mode 100644 index 0000000..8cf2b00 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/children/default-parent-key.md @@ -0,0 +1,35 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class GroupCreatedWithDefaultParentKey { + constructor(readonly name: string) {} +} + +@eventType() +class UserAddedWithDefaultParentKey { + constructor(readonly userId: string, readonly role: string) {} +} + +class GroupMemberWithDefaultParentKey { + userId = ''; + role = ''; +} + +class GroupWithDefaultParentKey { + name = ''; + members: GroupMemberWithDefaultParentKey[] = []; +} + +@projection() +class GroupWithDefaultParentKeyProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(GroupCreatedWithDefaultParentKey) + .children(m => m.members, children => children + .identifiedBy(m => m.userId) + .from(UserAddedWithDefaultParentKey, b => b + .usingKey(e => e.userId))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/children/event-parent-key-append.md b/Documentation/client-snippets/projections/declarative/children/event-parent-key-append.md new file mode 100644 index 0000000..ca4f9d0 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/children/event-parent-key-append.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class GroupMembershipWithEventParentKey { + constructor(private readonly eventStore: IEventStore) {} + + addUserToGroup(userId: string, groupId: string, role: string): Promise { + return this.eventStore.eventLog.append(userId, new UserAddedWithEventParentKey(groupId, userId, role)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/children/event-parent-key.md b/Documentation/client-snippets/projections/declarative/children/event-parent-key.md new file mode 100644 index 0000000..01c03f8 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/children/event-parent-key.md @@ -0,0 +1,36 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class GroupCreatedWithEventParentKey { + constructor(readonly name: string) {} +} + +@eventType() +class UserAddedWithEventParentKey { + constructor(readonly groupId: string, readonly userId: string, readonly role: string) {} +} + +class GroupMemberWithEventParentKey { + userId = ''; + role = ''; +} + +class GroupWithEventParentKey { + name = ''; + members: GroupMemberWithEventParentKey[] = []; +} + +@projection() +class GroupWithEventParentKeyProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(GroupCreatedWithEventParentKey) + .children(m => m.members, children => children + .identifiedBy(m => m.userId) + .from(UserAddedWithEventParentKey, b => b + .usingParentKey(e => e.groupId) + .usingKey(e => e.userId))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/children/events.md b/Documentation/client-snippets/projections/declarative/children/events.md new file mode 100644 index 0000000..2b7c933 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/children/events.md @@ -0,0 +1,23 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class GroupCreatedForChildEvents { + constructor(readonly name: string, readonly description: string) {} +} + +@eventType() +class UserAddedToGroupForChildEvents { + constructor(readonly userId: string, readonly role: string) {} +} + +@eventType() +class UserRoleChangedForChildEvents { + constructor(readonly userId: string, readonly role: string) {} +} + +@eventType() +class UserRemovedFromGroupForChildEvents { + constructor(readonly userId: string) {} +} +``` diff --git a/Documentation/client-snippets/projections/declarative/children/multiple-collections.md b/Documentation/client-snippets/projections/declarative/children/multiple-collections.md new file mode 100644 index 0000000..9d67524 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/children/multiple-collections.md @@ -0,0 +1,50 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class GroupCreatedWithMultipleCollections { + constructor(readonly name: string) {} +} + +@eventType() +class MemberAddedToGroup { + constructor(readonly userId: string, readonly role: string) {} +} + +@eventType() +class TaskAssignedToGroup { + constructor(readonly taskId: string, readonly title: string) {} +} + +class GroupMemberInMultipleCollections { + userId = ''; + role = ''; +} + +class GroupTaskInMultipleCollections { + taskId = ''; + title = ''; +} + +class GroupWithMultipleCollections { + name = ''; + members: GroupMemberInMultipleCollections[] = []; + tasks: GroupTaskInMultipleCollections[] = []; +} + +@projection() +class GroupWithMultipleCollectionsProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(GroupCreatedWithMultipleCollections) + .children(m => m.members, children => children + .identifiedBy(m => m.userId) + .from(MemberAddedToGroup, b => b + .usingKey(e => e.userId))) + .children(m => m.tasks, children => children + .identifiedBy(m => m.taskId) + .from(TaskAssignedToGroup, b => b + .usingKey(e => e.taskId))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/children/read-model.md b/Documentation/client-snippets/projections/declarative/children/read-model.md new file mode 100644 index 0000000..7663bbc --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/children/read-model.md @@ -0,0 +1,12 @@ +```typescript +class GroupMember { + userId = ''; + role = ''; +} + +class GroupWithMembers { + name = ''; + description = ''; + members: GroupMember[] = []; +} +``` diff --git a/Documentation/client-snippets/projections/declarative/children/removing.md b/Documentation/client-snippets/projections/declarative/children/removing.md new file mode 100644 index 0000000..ab47169 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/children/removing.md @@ -0,0 +1,42 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class GroupCreatedWithRemoval { + constructor(readonly name: string) {} +} + +@eventType() +class UserAddedWithRemoval { + constructor(readonly userId: string, readonly role: string) {} +} + +@eventType() +class UserRemovedWithRemoval { + constructor(readonly userId: string) {} +} + +class GroupMemberWithRemoval { + userId = ''; + role = ''; +} + +class GroupWithRemoval { + name = ''; + members: GroupMemberWithRemoval[] = []; +} + +@projection() +class GroupWithRemovalProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(GroupCreatedWithRemoval) + .children(m => m.members, children => children + .identifiedBy(m => m.userId) + .from(UserAddedWithRemoval, b => b + .usingKey(e => e.userId)) + .removedWith(UserRemovedWithRemoval, b => b + .usingKey(e => e.userId))); + } +} +``` From 5d8a4ea91fc720e4aa4247a8156ce0cbdcda0b99 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:39:07 +0200 Subject: [PATCH 25/39] Add TypeScript snippets for declarative projections (constant-key, functions, joins, from-every, event-context) Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../declarative/constant-key/basic.md | 22 ++++++++ .../constant-key/constant-parent-key.md | 29 ++++++++++ .../constant-key/multi-event-metrics.md | 40 ++++++++++++++ .../event-context/children-context.md | 32 +++++++++++ .../event-context/composite-key.md | 3 ++ .../declarative/from-every/with-children.md | 54 +++++++++++++++++++ .../declarative/functions/add-subtract.md | 37 +++++++++++++ .../declarative/functions/combined.md | 25 +++++++++ .../declarative/functions/count.md | 31 +++++++++++ .../projections/declarative/joins/basic.md | 25 +-------- .../declarative/joins/children-join.md | 37 +++++++++++++ .../projections/declarative/joins/events.md | 25 +++++++++ 12 files changed, 336 insertions(+), 24 deletions(-) create mode 100644 Documentation/client-snippets/projections/declarative/constant-key/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/constant-key/constant-parent-key.md create mode 100644 Documentation/client-snippets/projections/declarative/constant-key/multi-event-metrics.md create mode 100644 Documentation/client-snippets/projections/declarative/event-context/children-context.md create mode 100644 Documentation/client-snippets/projections/declarative/event-context/composite-key.md create mode 100644 Documentation/client-snippets/projections/declarative/from-every/with-children.md create mode 100644 Documentation/client-snippets/projections/declarative/functions/add-subtract.md create mode 100644 Documentation/client-snippets/projections/declarative/functions/combined.md create mode 100644 Documentation/client-snippets/projections/declarative/functions/count.md create mode 100644 Documentation/client-snippets/projections/declarative/joins/children-join.md create mode 100644 Documentation/client-snippets/projections/declarative/joins/events.md diff --git a/Documentation/client-snippets/projections/declarative/constant-key/basic.md b/Documentation/client-snippets/projections/declarative/constant-key/basic.md new file mode 100644 index 0000000..124e315 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/constant-key/basic.md @@ -0,0 +1,22 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecConstantKeyOrderPlaced { + constructor(readonly total: number) {} +} + +class DecConstantKeyGlobalCounter { + totalOrders = 0; +} + +@projection() +class DecConstantKeyGlobalCounterProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(DecConstantKeyOrderPlaced, _ => _ + .usingConstantKey('global') + .count(m => m.totalOrders)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/constant-key/constant-parent-key.md b/Documentation/client-snippets/projections/declarative/constant-key/constant-parent-key.md new file mode 100644 index 0000000..d9615fa --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/constant-key/constant-parent-key.md @@ -0,0 +1,29 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecConstantKeyUserJoined { + constructor(readonly userId: string, readonly userName: string) {} +} + +class DecConstantKeyTeamMember { + userId = ''; + name = ''; +} + +class DecConstantKeyTeam { + members: DecConstantKeyTeamMember[] = []; +} + +@projection() +class DecConstantKeyTeamActivityProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .children(m => m.members, children => children + .identifiedBy(e => e.userId) + .from(DecConstantKeyUserJoined, _ => _ + .usingConstantParentKey('main-team') + .set(m => m.name).to(e => e.userName))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/constant-key/multi-event-metrics.md b/Documentation/client-snippets/projections/declarative/constant-key/multi-event-metrics.md new file mode 100644 index 0000000..53ec34a --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/constant-key/multi-event-metrics.md @@ -0,0 +1,40 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecConstantKeyPageViewed { + constructor(readonly pageUrl: string) {} +} + +@eventType() +class DecConstantKeyButtonClicked { + constructor(readonly buttonId: string) {} +} + +@eventType() +class DecConstantKeyFormSubmitted { + constructor(readonly formId: string) {} +} + +class DecConstantKeyEngagementMetrics { + pageViews = 0; + buttonClicks = 0; + formSubmissions = 0; +} + +@projection() +class DecConstantKeyEngagementMetricsProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(DecConstantKeyPageViewed, _ => _ + .usingConstantKey('metrics') + .count(m => m.pageViews)) + .from(DecConstantKeyButtonClicked, _ => _ + .usingConstantKey('metrics') + .count(m => m.buttonClicks)) + .from(DecConstantKeyFormSubmitted, _ => _ + .usingConstantKey('metrics') + .count(m => m.formSubmissions)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/event-context/children-context.md b/Documentation/client-snippets/projections/declarative/event-context/children-context.md new file mode 100644 index 0000000..2611e39 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/event-context/children-context.md @@ -0,0 +1,32 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecEventContextActivityPerformed { + constructor(readonly activityId: string, readonly activityType: string) {} +} + +class DecEventContextActivityLogEntry { + activityId = ''; + timestamp = new Date(); + sequenceNumber = 0n; +} + +class DecEventContextUserWithActivityLog { + activityLog: DecEventContextActivityLogEntry[] = []; +} + +@projection() +class DecEventContextUserActivityLogProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .children(m => m.activityLog, children => children + .identifiedBy(e => e.activityId) + .autoMap() + .from(DecEventContextActivityPerformed, _ => _ + .usingKey(e => e.activityId) + .set(m => m.timestamp).toEventContextProperty('occurred') + .set(m => m.sequenceNumber).toEventContextProperty('sequenceNumber'))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/event-context/composite-key.md b/Documentation/client-snippets/projections/declarative/event-context/composite-key.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/event-context/composite-key.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/declarative/from-every/with-children.md b/Documentation/client-snippets/projections/declarative/from-every/with-children.md new file mode 100644 index 0000000..5fe6b84 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-every/with-children.md @@ -0,0 +1,54 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class OrderCreatedDeclarativeEveryChildren { + constructor(readonly orderNumber: string) {} +} + +@eventType() +class ItemAddedDeclarativeEveryChildren { + constructor( + readonly orderId: string, + readonly productId: string, + readonly productName: string, + readonly quantity: number + ) {} +} + +@eventType() +class ItemQuantityChangedDeclarativeEveryChildren { + constructor(readonly orderId: string, readonly productId: string, readonly quantity: number) {} +} + +class OrderItemDeclarativeEveryChildren { + productId = ''; + productName = ''; + quantity = 0; +} + +class OrderDeclarativeEveryChildren { + orderNumber = ''; + lastModified = new Date(); + items: OrderItemDeclarativeEveryChildren[] = []; +} + +@projection() +class OrderDeclarativeEveryChildrenProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(OrderCreatedDeclarativeEveryChildren) + .fromEvery(_ => _ + .set(m => m.lastModified) + .toEventContextProperty('occurred')) + .children(m => m.items, children => children + .identifiedBy(m => m.productId) + .from(ItemAddedDeclarativeEveryChildren, _ => _ + .usingKey(e => e.productId) + .usingParentKey(e => e.orderId)) + .from(ItemQuantityChangedDeclarativeEveryChildren, _ => _ + .usingKey(e => e.productId) + .usingParentKey(e => e.orderId))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/functions/add-subtract.md b/Documentation/client-snippets/projections/declarative/functions/add-subtract.md new file mode 100644 index 0000000..b0bf08a --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/functions/add-subtract.md @@ -0,0 +1,37 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecFunctionsAccountOpened { + constructor(readonly number: string) {} +} + +@eventType() +class DecFunctionsMoneyDeposited { + constructor(readonly amount: number) {} +} + +@eventType() +class DecFunctionsMoneyWithdrawn { + constructor(readonly amount: number) {} +} + +class DecFunctionsAccount { + number = ''; + balance = 0; +} + +@projection() +class DecFunctionsAccountProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() + .from(DecFunctionsAccountOpened, _ => _ + .set(m => m.balance).toValue(0)) + .from(DecFunctionsMoneyDeposited, _ => _ + .add(m => m.balance).with(e => e.amount)) + .from(DecFunctionsMoneyWithdrawn, _ => _ + .subtract(m => m.balance).with(e => e.amount)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/functions/combined.md b/Documentation/client-snippets/projections/declarative/functions/combined.md new file mode 100644 index 0000000..bb8f0d5 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/functions/combined.md @@ -0,0 +1,25 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecFunctionsTransaction { + constructor(readonly amount: number) {} +} + +class DecFunctionsTransactionSummary { + transactionCount = 0; + totalAmount = 0; + processedEvents = 0; +} + +@projection() +class DecFunctionsTransactionSummaryProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(DecFunctionsTransaction, _ => _ + .count(m => m.transactionCount) + .add(m => m.totalAmount).with(e => e.amount) + .increment(m => m.processedEvents)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/functions/count.md b/Documentation/client-snippets/projections/declarative/functions/count.md new file mode 100644 index 0000000..552c894 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/functions/count.md @@ -0,0 +1,31 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecFunctionsUserLoggedIn { + constructor(readonly username: string) {} +} + +@eventType() +class DecFunctionsUserPerformedAction { + constructor(readonly username: string, readonly actionType: string) {} +} + +class DecFunctionsUserActivity { + username = ''; + loginCount = 0; + actionCount = 0; +} + +@projection() +class DecFunctionsUserActivityProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() + .from(DecFunctionsUserLoggedIn, _ => _ + .count(m => m.loginCount)) + .from(DecFunctionsUserPerformedAction, _ => _ + .count(m => m.actionCount)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/joins/basic.md b/Documentation/client-snippets/projections/declarative/joins/basic.md index de7b505..7460f76 100644 --- a/Documentation/client-snippets/projections/declarative/joins/basic.md +++ b/Documentation/client-snippets/projections/declarative/joins/basic.md @@ -1,28 +1,5 @@ ```typescript -import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; - -@eventType() -class DecJoinsUserCreated { - name = ''; - email = ''; -} - -@eventType() -class DecJoinsUserAssignedToGroup { - userId = ''; - groupId = ''; -} - -@eventType() -class DecJoinsGroupCreated { - name = ''; - description = ''; -} - -@eventType() -class DecJoinsGroupRenamed { - newName = ''; -} +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; @projection() class DecJoinsUserProjection implements IProjectionFor { diff --git a/Documentation/client-snippets/projections/declarative/joins/children-join.md b/Documentation/client-snippets/projections/declarative/joins/children-join.md new file mode 100644 index 0000000..ece35b0 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/joins/children-join.md @@ -0,0 +1,37 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecJoinsChildTaskAssigned { + constructor(readonly taskId: string, readonly projectId: string) {} +} + +@eventType() +class DecJoinsChildProjectCreated { + constructor(readonly name: string) {} +} + +class DecJoinsChildTask { + taskId = ''; + projectId = ''; + projectName: string | null = null; +} + +class DecJoinsChildProjectBoard { + tasks: DecJoinsChildTask[] = []; +} + +@projection() +class DecJoinsChildProjectBoardProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .children(m => m.tasks, children => children + .identifiedBy(e => e.taskId) + .autoMap() + .from(DecJoinsChildTaskAssigned, b => b + .usingKey(e => e.taskId)) + .join(DecJoinsChildProjectCreated, j => j + .on(m => m.projectId))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/joins/events.md b/Documentation/client-snippets/projections/declarative/joins/events.md new file mode 100644 index 0000000..a93ac2f --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/joins/events.md @@ -0,0 +1,25 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +// User stream events +@eventType() +class DecJoinsUserCreated { + constructor(readonly name: string, readonly email: string) {} +} + +@eventType() +class DecJoinsUserAssignedToGroup { + constructor(readonly userId: string, readonly groupId: string) {} +} + +// Group stream events +@eventType() +class DecJoinsGroupCreated { + constructor(readonly name: string, readonly description: string) {} +} + +@eventType() +class DecJoinsGroupRenamed { + constructor(readonly newName: string) {} +} +``` From f970d6f92addd0aa9eec76c81d49be58e139ec1f Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:40:34 +0200 Subject: [PATCH 26/39] Add TypeScript snippets for declarative nested projections Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../projections/declarative/nested/automap.md | 44 +++++++++++++++ .../projections/declarative/nested/basic.md | 38 +++++++++++++ .../nested/employee-contract-events.md | 27 +++++++++ .../declarative/nested/employee-contract.md | 53 ++++++++++++++++++ .../projections/declarative/nested/events.md | 17 ++++++ .../nested/multiple-from-events.md | 52 ++++++++++++++++++ .../declarative/nested/multiple-nested.md | 55 +++++++++++++++++++ .../declarative/nested/nested-in-children.md | 55 +++++++++++++++++++ .../declarative/nested/product-promotion.md | 40 ++++++++++++++ .../declarative/nested/read-model.md | 11 ++++ 10 files changed, 392 insertions(+) create mode 100644 Documentation/client-snippets/projections/declarative/nested/automap.md create mode 100644 Documentation/client-snippets/projections/declarative/nested/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/nested/employee-contract-events.md create mode 100644 Documentation/client-snippets/projections/declarative/nested/employee-contract.md create mode 100644 Documentation/client-snippets/projections/declarative/nested/events.md create mode 100644 Documentation/client-snippets/projections/declarative/nested/multiple-from-events.md create mode 100644 Documentation/client-snippets/projections/declarative/nested/multiple-nested.md create mode 100644 Documentation/client-snippets/projections/declarative/nested/nested-in-children.md create mode 100644 Documentation/client-snippets/projections/declarative/nested/product-promotion.md create mode 100644 Documentation/client-snippets/projections/declarative/nested/read-model.md diff --git a/Documentation/client-snippets/projections/declarative/nested/automap.md b/Documentation/client-snippets/projections/declarative/nested/automap.md new file mode 100644 index 0000000..1522ca3 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/nested/automap.md @@ -0,0 +1,44 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class SliceCreatedForNestedAutoMap { + constructor(readonly name: string) {} +} + +@eventType() +class CommandSetForNestedAutoMap { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class CommandUpdatedForNestedAutoMap { + constructor(readonly schema: string) {} +} + +@eventType() +class CommandClearedForNestedAutoMap { +} + +class CommandItemForNestedAutoMap { + name = ''; + schema = ''; +} + +class SliceForNestedAutoMap { + name = ''; + command: CommandItemForNestedAutoMap | null = null; +} + +@projection() +class SliceProjectionForNestedAutoMap implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(SliceCreatedForNestedAutoMap) + .nested(m => m.command, nested => nested + .from(CommandSetForNestedAutoMap) + .from(CommandUpdatedForNestedAutoMap) + .clearWith(CommandClearedForNestedAutoMap)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/nested/basic.md b/Documentation/client-snippets/projections/declarative/nested/basic.md new file mode 100644 index 0000000..9af4096 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/nested/basic.md @@ -0,0 +1,38 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class SliceCreatedForNestedBasic { + constructor(readonly name: string) {} +} + +@eventType() +class CommandSetForDeclarativeNestedBasic { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class CommandClearedForDeclarativeNestedBasic { +} + +class CommandItemForNestedBasic { + name = ''; + schema = ''; +} + +class SliceForNestedBasic { + name = ''; + command: CommandItemForNestedBasic | null = null; +} + +@projection() +class SliceProjectionForNestedBasic implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(SliceCreatedForNestedBasic) + .nested(m => m.command, nested => nested + .from(CommandSetForDeclarativeNestedBasic) + .clearWith(CommandClearedForDeclarativeNestedBasic)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/nested/employee-contract-events.md b/Documentation/client-snippets/projections/declarative/nested/employee-contract-events.md new file mode 100644 index 0000000..b1e2acc --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/nested/employee-contract-events.md @@ -0,0 +1,27 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class EmployeeHiredForNestedContractEvents { + constructor(readonly name: string, readonly department: string) {} +} + +@eventType() +class ContractStartedForNestedContractEvents { + constructor( + readonly contractId: string, + readonly startDate: string, + readonly endDate: string, + readonly type: string + ) {} +} + +@eventType() +class ContractExtendedForNestedContractEvents { + constructor(readonly newEndDate: string) {} +} + +@eventType() +class ContractEndedForNestedContractEvents { +} +``` diff --git a/Documentation/client-snippets/projections/declarative/nested/employee-contract.md b/Documentation/client-snippets/projections/declarative/nested/employee-contract.md new file mode 100644 index 0000000..6a83505 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/nested/employee-contract.md @@ -0,0 +1,53 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class EmployeeHiredWithNestedContract { + constructor(readonly name: string, readonly department: string) {} +} + +@eventType() +class ContractStartedWithNestedContract { + constructor( + readonly contractId: string, + readonly startDate: string, + readonly endDate: string, + readonly type: string + ) {} +} + +@eventType() +class ContractExtendedWithNestedContract { + constructor(readonly newEndDate: string) {} +} + +@eventType() +class ContractEndedWithNestedContract { +} + +class ContractForNestedEmployee { + contractId = ''; + startDate = ''; + endDate = ''; + type = ''; +} + +class EmployeeWithNestedContract { + name = ''; + department = ''; + activeContract: ContractForNestedEmployee | null = null; +} + +@projection() +class EmployeeProjectionWithNestedContract implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(EmployeeHiredWithNestedContract) + .nested(m => m.activeContract, contract => contract + .from(ContractStartedWithNestedContract) + .from(ContractExtendedWithNestedContract, b => b + .set(m => m.endDate).to(e => e.newEndDate)) + .clearWith(ContractEndedWithNestedContract)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/nested/events.md b/Documentation/client-snippets/projections/declarative/nested/events.md new file mode 100644 index 0000000..a92bc98 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/nested/events.md @@ -0,0 +1,17 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class SliceCreatedForNestedEvents { + constructor(readonly name: string) {} +} + +@eventType() +class CommandSetForNestedEvents { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class CommandClearedForNestedEvents { +} +``` diff --git a/Documentation/client-snippets/projections/declarative/nested/multiple-from-events.md b/Documentation/client-snippets/projections/declarative/nested/multiple-from-events.md new file mode 100644 index 0000000..f552870 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/nested/multiple-from-events.md @@ -0,0 +1,52 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class SliceCreatedForNestedUpdates { + constructor(readonly name: string) {} +} + +@eventType() +class CommandSetForNestedUpdates { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class CommandRenamedForNestedUpdates { + constructor(readonly newName: string) {} +} + +@eventType() +class CommandSchemaUpdatedForNestedUpdates { + constructor(readonly updatedSchema: string) {} +} + +@eventType() +class CommandClearedForNestedUpdates { +} + +class CommandItemForNestedUpdates { + name = ''; + schema = ''; +} + +class SliceForNestedUpdates { + name = ''; + command: CommandItemForNestedUpdates | null = null; +} + +@projection() +class SliceProjectionForNestedUpdates implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(SliceCreatedForNestedUpdates) + .nested(m => m.command, nested => nested + .from(CommandSetForNestedUpdates) + .from(CommandRenamedForNestedUpdates, b => b + .set(m => m.name).to(e => e.newName)) + .from(CommandSchemaUpdatedForNestedUpdates, b => b + .set(m => m.schema).to(e => e.updatedSchema)) + .clearWith(CommandClearedForNestedUpdates)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/nested/multiple-nested.md b/Documentation/client-snippets/projections/declarative/nested/multiple-nested.md new file mode 100644 index 0000000..fc628a0 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/nested/multiple-nested.md @@ -0,0 +1,55 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class SliceCreatedWithMultipleNested { + constructor(readonly name: string) {} +} + +@eventType() +class CommandSetWithMultipleNested { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class CommandClearedWithMultipleNested { +} + +@eventType() +class ValidationConfiguredWithMultipleNested { + constructor(readonly ruleName: string) {} +} + +@eventType() +class ValidationRemovedWithMultipleNested { +} + +class CommandItemWithMultipleNested { + name = ''; + schema = ''; +} + +class ValidationConfigWithMultipleNested { + ruleName = ''; +} + +class SliceWithMultipleNested { + name = ''; + command: CommandItemWithMultipleNested | null = null; + validation: ValidationConfigWithMultipleNested | null = null; +} + +@projection() +class SliceProjectionWithMultipleNested implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(SliceCreatedWithMultipleNested) + .nested(m => m.command, nested => nested + .from(CommandSetWithMultipleNested) + .clearWith(CommandClearedWithMultipleNested)) + .nested(m => m.validation, nested => nested + .from(ValidationConfiguredWithMultipleNested) + .clearWith(ValidationRemovedWithMultipleNested)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/nested/nested-in-children.md b/Documentation/client-snippets/projections/declarative/nested/nested-in-children.md new file mode 100644 index 0000000..72e34d3 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/nested/nested-in-children.md @@ -0,0 +1,55 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class ProjectCreatedWithNestedChildren { + constructor(readonly name: string) {} +} + +@eventType() +class TaskAddedWithNestedChild { + constructor(readonly taskId: string, readonly title: string) {} +} + +@eventType() +class TaskAssignedWithNestedChild { + constructor(readonly taskId: string, readonly name: string, readonly email: string) {} +} + +@eventType() +class TaskUnassignedWithNestedChild { + constructor(readonly taskId: string) {} +} + +class AssigneeForNestedChild { + name = ''; + email = ''; +} + +class TaskWithNestedAssignee { + taskId = ''; + title = ''; + assignee: AssigneeForNestedChild | null = null; +} + +class ProjectWithDeclarativeNestedChildren { + name = ''; + tasks: TaskWithNestedAssignee[] = []; +} + +@projection() +class ProjectProjectionWithDeclarativeNestedChildren implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(ProjectCreatedWithNestedChildren) + .children(m => m.tasks, tasks => tasks + .identifiedBy(m => m.taskId) + .from(TaskAddedWithNestedChild, b => b + .usingKey(e => e.taskId)) + .nested(m => m.assignee, assignee => assignee + .from(TaskAssignedWithNestedChild, b => b + .usingKey(e => e.taskId)) + .clearWith(TaskUnassignedWithNestedChild))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/nested/product-promotion.md b/Documentation/client-snippets/projections/declarative/nested/product-promotion.md new file mode 100644 index 0000000..bad98a6 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/nested/product-promotion.md @@ -0,0 +1,40 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class ProductListedWithNestedPromotion { + constructor(readonly name: string, readonly basePrice: number) {} +} + +@eventType() +class PromotionAppliedWithNestedPromotion { + constructor(readonly label: string, readonly discountPercent: number, readonly validUntil: Date) {} +} + +@eventType() +class PromotionRemovedWithNestedPromotion { +} + +class PromotionForNestedProduct { + label = ''; + discountPercent = 0; + validUntil = new Date(); +} + +class ProductWithNestedPromotion { + name = ''; + basePrice = 0; + promotion: PromotionForNestedProduct | null = null; +} + +@projection() +class ProductProjectionWithNestedPromotion implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(ProductListedWithNestedPromotion) + .nested(m => m.promotion, promotion => promotion + .from(PromotionAppliedWithNestedPromotion) + .clearWith(PromotionRemovedWithNestedPromotion)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/nested/read-model.md b/Documentation/client-snippets/projections/declarative/nested/read-model.md new file mode 100644 index 0000000..29527d5 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/nested/read-model.md @@ -0,0 +1,11 @@ +```typescript +class CommandItemForNestedCommand { + name = ''; + schema = ''; +} + +class SliceWithNestedCommand { + name = ''; + command: CommandItemForNestedCommand | null = null; +} +``` From ecfbdfa3940d7e88bd8420ae1edee787bf8791ab Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:41:30 +0200 Subject: [PATCH 27/39] Add TypeScript snippets for declarative remove-with-join projections Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../declarative/remove-with-join/basic.md | 51 ++++++++++++++++ .../declarative/remove-with-join/events.md | 60 +++++++++++++++++++ .../remove-with-join/explicit-keys.md | 54 +++++++++++++++++ .../project-assignments-example.md | 27 +++++++++ .../remove-with-join/read-models.md | 35 +++++++++++ .../remove-with-join/user-groups-example.md | 26 ++++++++ 6 files changed, 253 insertions(+) create mode 100644 Documentation/client-snippets/projections/declarative/remove-with-join/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/remove-with-join/events.md create mode 100644 Documentation/client-snippets/projections/declarative/remove-with-join/explicit-keys.md create mode 100644 Documentation/client-snippets/projections/declarative/remove-with-join/project-assignments-example.md create mode 100644 Documentation/client-snippets/projections/declarative/remove-with-join/read-models.md create mode 100644 Documentation/client-snippets/projections/declarative/remove-with-join/user-groups-example.md diff --git a/Documentation/client-snippets/projections/declarative/remove-with-join/basic.md b/Documentation/client-snippets/projections/declarative/remove-with-join/basic.md new file mode 100644 index 0000000..186ca7f --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/remove-with-join/basic.md @@ -0,0 +1,51 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecRemoveWithJoinBasicUserCreated { + constructor(readonly name: string) {} +} + +@eventType() +class DecRemoveWithJoinBasicUserAddedToGroup { + constructor(readonly userId: string, readonly groupId: string) {} +} + +@eventType() +class DecRemoveWithJoinBasicGroupCreated { + constructor(readonly name: string) {} +} + +@eventType() +class DecRemoveWithJoinBasicGroupDeleted { +} + +class DecRemoveWithJoinBasicUserGroup { + groupId = ''; + name = ''; + joinedAt = new Date(); +} + +class DecRemoveWithJoinBasicUser { + name = ''; + groups: DecRemoveWithJoinBasicUserGroup[] = []; +} + +@projection() +class DecRemoveWithJoinBasicUserProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() + .from(DecRemoveWithJoinBasicUserCreated) + .children(m => m.groups, children => children + .identifiedBy(e => e.groupId) + .autoMap() + .from(DecRemoveWithJoinBasicUserAddedToGroup, _ => _ + .usingParentKey(e => e.userId) + .set(m => m.joinedAt).toEventContextProperty('occurred')) + .join(DecRemoveWithJoinBasicGroupCreated, _ => _ + .on(m => m.groupId)) + .removedWithJoin(DecRemoveWithJoinBasicGroupDeleted)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/remove-with-join/events.md b/Documentation/client-snippets/projections/declarative/remove-with-join/events.md new file mode 100644 index 0000000..1e95abf --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/remove-with-join/events.md @@ -0,0 +1,60 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class DecRemoveWithJoinUserRegistered { + constructor(readonly username: string, readonly email: string) {} +} + +@eventType() +class DecRemoveWithJoinUserJoinedGroup { + constructor(readonly userId: string, readonly groupId: string, readonly role: string) {} +} + +@eventType() +class DecRemoveWithJoinUserLeftGroup { + constructor(readonly userId: string, readonly groupId: string) {} +} + +@eventType() +class DecRemoveWithJoinGroupCreated { + constructor(readonly groupName: string, readonly groupType: string) {} +} + +@eventType() +class DecRemoveWithJoinGroupDisbanded { +} + +@eventType() +class DecRemoveWithJoinDeveloperOnboarded { + constructor(readonly name: string, readonly skills: string[]) {} +} + +@eventType() +class DecRemoveWithJoinDeveloperAssignedToProject { + constructor( + readonly developerId: string, + readonly projectId: string, + readonly role: string, + readonly allocation: number + ) {} +} + +@eventType() +class DecRemoveWithJoinDeveloperUnassignedFromProject { + constructor(readonly developerId: string, readonly projectId: string) {} +} + +@eventType() +class DecRemoveWithJoinProjectInitiated { + constructor(readonly projectName: string, readonly priority: string, readonly deadline: Date) {} +} + +@eventType() +class DecRemoveWithJoinProjectCancelled { +} + +@eventType() +class DecRemoveWithJoinProjectCompleted { +} +``` diff --git a/Documentation/client-snippets/projections/declarative/remove-with-join/explicit-keys.md b/Documentation/client-snippets/projections/declarative/remove-with-join/explicit-keys.md new file mode 100644 index 0000000..713033d --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/remove-with-join/explicit-keys.md @@ -0,0 +1,54 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecRemoveWithJoinExplicitEmployeeHired { + constructor(readonly name: string) {} +} + +@eventType() +class DecRemoveWithJoinExplicitEmployeeAssignedToProject { + constructor(readonly employeeId: string, readonly projectId: string) {} +} + +@eventType() +class DecRemoveWithJoinExplicitProjectCreated { + constructor(readonly name: string) {} +} + +@eventType() +class DecRemoveWithJoinExplicitProjectCancelled { + constructor(readonly projectId: string) {} +} + +class DecRemoveWithJoinExplicitEmployeeProject { + projectId = ''; + name = ''; + assignedAt = new Date(); +} + +class DecRemoveWithJoinExplicitEmployee { + name = ''; + projects: DecRemoveWithJoinExplicitEmployeeProject[] = []; +} + +@projection() +class DecRemoveWithJoinExplicitEmployeeProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() + .from(DecRemoveWithJoinExplicitEmployeeHired) + .children(m => m.projects, children => children + .identifiedBy(e => e.projectId) + .autoMap() + .from(DecRemoveWithJoinExplicitEmployeeAssignedToProject, _ => _ + .usingParentKey(e => e.employeeId) + .usingKey(e => e.projectId) + .set(m => m.assignedAt).toEventContextProperty('occurred')) + .join(DecRemoveWithJoinExplicitProjectCreated, _ => _ + .on(m => m.projectId)) + .removedWithJoin(DecRemoveWithJoinExplicitProjectCancelled, _ => _ + .usingKey(e => e.projectId))); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/remove-with-join/project-assignments-example.md b/Documentation/client-snippets/projections/declarative/remove-with-join/project-assignments-example.md new file mode 100644 index 0000000..0eaf1f0 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/remove-with-join/project-assignments-example.md @@ -0,0 +1,27 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@projection() +class DecRemoveWithJoinDeveloperProjectsProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() + .from(DecRemoveWithJoinDeveloperOnboarded, _ => _ + .set(m => m.developerId).toEventSourceId() + .set(m => m.onboardedAt).toEventContextProperty('occurred')) + .children(m => m.currentProjects, children => children + .identifiedBy(e => e.projectId) + .autoMap() + .from(DecRemoveWithJoinDeveloperAssignedToProject, _ => _ + .usingParentKey(e => e.developerId) + .usingKey(e => e.projectId) + .set(m => m.assignedAt).toEventContextProperty('occurred')) + .join(DecRemoveWithJoinProjectInitiated, _ => _ + .on(m => m.projectId)) + .removedWith(DecRemoveWithJoinDeveloperUnassignedFromProject, _ => _ + .usingKey(e => e.projectId)) + .removedWithJoin(DecRemoveWithJoinProjectCancelled) + .removedWithJoin(DecRemoveWithJoinProjectCompleted)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/remove-with-join/read-models.md b/Documentation/client-snippets/projections/declarative/remove-with-join/read-models.md new file mode 100644 index 0000000..b2d12ee --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/remove-with-join/read-models.md @@ -0,0 +1,35 @@ +```typescript +class DecRemoveWithJoinGroupMembership { + groupId = ''; + groupName = ''; + groupType = ''; + joinedAt = new Date(); + role = ''; +} + +class DecRemoveWithJoinUserProfile { + userId = ''; + username = ''; + email = ''; + registeredAt = new Date(); + memberships: DecRemoveWithJoinGroupMembership[] = []; +} + +class DecRemoveWithJoinProjectAssignment { + projectId = ''; + projectName = ''; + priority = ''; + deadline = new Date(); + assignedAt = new Date(); + role = ''; + allocation = 0; +} + +class DecRemoveWithJoinDeveloperProfile { + developerId = ''; + name = ''; + skills: string[] = []; + onboardedAt = new Date(); + currentProjects: DecRemoveWithJoinProjectAssignment[] = []; +} +``` diff --git a/Documentation/client-snippets/projections/declarative/remove-with-join/user-groups-example.md b/Documentation/client-snippets/projections/declarative/remove-with-join/user-groups-example.md new file mode 100644 index 0000000..d106833 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/remove-with-join/user-groups-example.md @@ -0,0 +1,26 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@projection() +class DecRemoveWithJoinGroupMembershipProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() + .from(DecRemoveWithJoinUserRegistered, _ => _ + .set(m => m.userId).toEventSourceId() + .set(m => m.registeredAt).toEventContextProperty('occurred')) + .children(m => m.memberships, children => children + .identifiedBy(e => e.groupId) + .autoMap() + .from(DecRemoveWithJoinUserJoinedGroup, _ => _ + .usingParentKey(e => e.userId) + .usingKey(e => e.groupId) + .set(m => m.joinedAt).toEventContextProperty('occurred')) + .join(DecRemoveWithJoinGroupCreated, _ => _ + .on(m => m.groupId)) + .removedWith(DecRemoveWithJoinUserLeftGroup, _ => _ + .usingKey(e => e.groupId)) + .removedWithJoin(DecRemoveWithJoinGroupDisbanded)); + } +} +``` From da176c870f8e67d0cc249f45738e8a0f9e6640c5 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:41:56 +0200 Subject: [PATCH 28/39] Add TypeScript snippets for declarative auto-map and filtering docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../declarative/auto-map/aggregate-only.md | 28 +++++++++++++ .../auto-map/re-enable-child-scope.md | 40 +++++++++++++++++++ .../projections/filtering/tagging.md | 3 ++ .../projections/filtering/with-reactor.md | 3 ++ .../projections/filtering/with-reducer.md | 3 ++ 5 files changed, 77 insertions(+) create mode 100644 Documentation/client-snippets/projections/declarative/auto-map/aggregate-only.md create mode 100644 Documentation/client-snippets/projections/declarative/auto-map/re-enable-child-scope.md create mode 100644 Documentation/client-snippets/projections/filtering/tagging.md create mode 100644 Documentation/client-snippets/projections/filtering/with-reactor.md create mode 100644 Documentation/client-snippets/projections/filtering/with-reducer.md diff --git a/Documentation/client-snippets/projections/declarative/auto-map/aggregate-only.md b/Documentation/client-snippets/projections/declarative/auto-map/aggregate-only.md new file mode 100644 index 0000000..b006e94 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/auto-map/aggregate-only.md @@ -0,0 +1,28 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DeclAggArrangementSet { + constructor(readonly location: string) {} +} + +@eventType() +class DeclAggCandidateSubmitted { + constructor(readonly name: string, readonly location: string) {} +} + +class DeclAggAssignmentSummary { + location = ''; + candidateCount = 0; +} + +@projection() +class DeclAggAssignmentProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(DeclAggArrangementSet) + .from(DeclAggCandidateSubmitted, _ => _ + .count(m => m.candidateCount)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/auto-map/re-enable-child-scope.md b/Documentation/client-snippets/projections/declarative/auto-map/re-enable-child-scope.md new file mode 100644 index 0000000..5923bd4 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/auto-map/re-enable-child-scope.md @@ -0,0 +1,40 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class AutoMapTeamFormed { + constructor(readonly teamName: string) {} +} + +@eventType() +class AutoMapMemberJoinedTeam { + constructor(readonly memberId: string, readonly displayName: string) {} +} + +class AutoMapTeamMember { + memberId = ''; + displayName = ''; +} + +class AutoMapTeam { + name = ''; + createdAt = new Date(); + members: AutoMapTeamMember[] = []; +} + +@projection() +class AutoMapTeamProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .noAutoMap() + .from(AutoMapTeamFormed, _ => _ + .set(m => m.name).to(e => e.teamName) + .set(m => m.createdAt).toEventContextProperty('occurred')) + .children(m => m.members, children => children + .identifiedBy(m => m.memberId) + .autoMap() + .from(AutoMapMemberJoinedTeam, _ => _ + .usingKey(e => e.memberId))); + } +} +``` diff --git a/Documentation/client-snippets/projections/filtering/tagging.md b/Documentation/client-snippets/projections/filtering/tagging.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/filtering/tagging.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/filtering/with-reactor.md b/Documentation/client-snippets/projections/filtering/with-reactor.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/filtering/with-reactor.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/filtering/with-reducer.md b/Documentation/client-snippets/projections/filtering/with-reducer.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/filtering/with-reducer.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 9b3adea691b0f2c31c15b79d48b9d6ba43f607fa Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:45:16 +0200 Subject: [PATCH 29/39] Add TypeScript snippets for model-bound and PDL projections docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../model-bound/children/no-automap.md | 3 + .../projections/model-bound/clearing/basic.md | 3 + .../projections/model-bound/clearing/child.md | 3 + .../model-bound/clearing/fluent.md | 3 + .../model-bound/clearing/nested-member.md | 3 + .../model-bound/clearing/nullable.md | 3 + .../model-bound/clearing/set-value-null.md | 21 ++++++ .../convention-based/aggregate-only.md | 27 +++++++ .../convention-based/no-auto-map-property.md | 3 + .../event-sequence-source/event-log.md | 3 + .../event-sequence-source/model-bound.md | 3 + .../from-all/attribute-convention.md | 3 + .../model-bound/from-all/fluent-context.md | 31 ++++++++ .../model-bound/nested/basic-lifecycle.md | 29 ++++++++ .../model-bound/nested/clear-with.md | 19 +++++ .../model-bound/nested/complete.md | 73 +++++++++++++++++++ .../model-bound/nested/explicit-mapping.md | 29 ++++++++ .../nested/multiple-clear-events.md | 24 ++++++ .../nested/multiple-from-events.md | 31 ++++++++ .../model-bound/nested/multiple-nested.md | 45 ++++++++++++ .../model-bound/nested/nested-in-children.md | 41 +++++++++++ .../model-bound/nested/no-automap.md | 3 + .../model-bound/nested/parent-property.md | 13 ++++ .../adhoc-querying/basic.md | 3 + .../adhoc-querying/custom-sequence.md | 3 + .../adhoc-querying/error-handling.md | 3 + .../adhoc-querying/inferred-vs-explicit.md | 3 + .../adhoc-querying/type-mismatch.md | 3 + .../tagging-projections/mixed-approach.md | 3 + .../tagging-projections/model-bound.md | 3 + .../multiple-tags-multiple-attributes.md | 3 + .../multiple-tags-single-attribute.md | 3 + .../tagging-projections/single-tag.md | 3 + .../tagging-projections/tag-categories.md | 3 + 34 files changed, 449 insertions(+) create mode 100644 Documentation/client-snippets/projections/model-bound/children/no-automap.md create mode 100644 Documentation/client-snippets/projections/model-bound/clearing/basic.md create mode 100644 Documentation/client-snippets/projections/model-bound/clearing/child.md create mode 100644 Documentation/client-snippets/projections/model-bound/clearing/fluent.md create mode 100644 Documentation/client-snippets/projections/model-bound/clearing/nested-member.md create mode 100644 Documentation/client-snippets/projections/model-bound/clearing/nullable.md create mode 100644 Documentation/client-snippets/projections/model-bound/clearing/set-value-null.md create mode 100644 Documentation/client-snippets/projections/model-bound/convention-based/aggregate-only.md create mode 100644 Documentation/client-snippets/projections/model-bound/convention-based/no-auto-map-property.md create mode 100644 Documentation/client-snippets/projections/model-bound/event-sequence-source/event-log.md create mode 100644 Documentation/client-snippets/projections/model-bound/event-sequence-source/model-bound.md create mode 100644 Documentation/client-snippets/projections/model-bound/from-all/attribute-convention.md create mode 100644 Documentation/client-snippets/projections/model-bound/from-all/fluent-context.md create mode 100644 Documentation/client-snippets/projections/model-bound/nested/basic-lifecycle.md create mode 100644 Documentation/client-snippets/projections/model-bound/nested/clear-with.md create mode 100644 Documentation/client-snippets/projections/model-bound/nested/complete.md create mode 100644 Documentation/client-snippets/projections/model-bound/nested/explicit-mapping.md create mode 100644 Documentation/client-snippets/projections/model-bound/nested/multiple-clear-events.md create mode 100644 Documentation/client-snippets/projections/model-bound/nested/multiple-from-events.md create mode 100644 Documentation/client-snippets/projections/model-bound/nested/multiple-nested.md create mode 100644 Documentation/client-snippets/projections/model-bound/nested/nested-in-children.md create mode 100644 Documentation/client-snippets/projections/model-bound/nested/no-automap.md create mode 100644 Documentation/client-snippets/projections/model-bound/nested/parent-property.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/basic.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/custom-sequence.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/error-handling.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/inferred-vs-explicit.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/type-mismatch.md create mode 100644 Documentation/client-snippets/projections/tagging-projections/mixed-approach.md create mode 100644 Documentation/client-snippets/projections/tagging-projections/model-bound.md create mode 100644 Documentation/client-snippets/projections/tagging-projections/multiple-tags-multiple-attributes.md create mode 100644 Documentation/client-snippets/projections/tagging-projections/multiple-tags-single-attribute.md create mode 100644 Documentation/client-snippets/projections/tagging-projections/single-tag.md create mode 100644 Documentation/client-snippets/projections/tagging-projections/tag-categories.md diff --git a/Documentation/client-snippets/projections/model-bound/children/no-automap.md b/Documentation/client-snippets/projections/model-bound/children/no-automap.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/children/no-automap.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/clearing/basic.md b/Documentation/client-snippets/projections/model-bound/clearing/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/clearing/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/clearing/child.md b/Documentation/client-snippets/projections/model-bound/clearing/child.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/clearing/child.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/clearing/fluent.md b/Documentation/client-snippets/projections/model-bound/clearing/fluent.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/clearing/fluent.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/clearing/nested-member.md b/Documentation/client-snippets/projections/model-bound/clearing/nested-member.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/clearing/nested-member.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/clearing/nullable.md b/Documentation/client-snippets/projections/model-bound/clearing/nullable.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/clearing/nullable.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/clearing/set-value-null.md b/Documentation/client-snippets/projections/model-bound/clearing/set-value-null.md new file mode 100644 index 0000000..ecbb435 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/clearing/set-value-null.md @@ -0,0 +1,21 @@ +```typescript +import { eventType, fromEvent, Guid, readModel, setValue } from '@cratis/chronicle'; + +@eventType() +class MbClearingInvoiceIssued { + constructor(readonly reference: string) {} +} + +@eventType() +class MbClearingInvoiceVoided { +} + +@readModel() +@fromEvent(MbClearingInvoiceIssued) +class MbClearingInvoice { + id: Guid = Guid.empty; + + @setValue(MbClearingInvoiceVoided, null) + reference: string | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/convention-based/aggregate-only.md b/Documentation/client-snippets/projections/model-bound/convention-based/aggregate-only.md new file mode 100644 index 0000000..c96437d --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/convention-based/aggregate-only.md @@ -0,0 +1,27 @@ +```typescript +import { count, eventType, fromEvent, Guid, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class AggOnlyArrangementSet { + constructor(readonly location: string) {} +} + +@eventType() +class AggOnlyCandidateSubmitted { + constructor(readonly name: string, readonly location: string) {} +} + +// AggOnlyCandidateSubmitted is subscribed only to be counted, so its identically named +// location is not auto-mapped over the value sourced from AggOnlyArrangementSet. +@readModel() +@fromEvent(AggOnlyArrangementSet) +class AggOnlyAssignmentSummary { + id: Guid = Guid.empty; + + @setFrom(AggOnlyArrangementSet, 'location') + location = ''; + + @count(AggOnlyCandidateSubmitted) + candidateCount = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/convention-based/no-auto-map-property.md b/Documentation/client-snippets/projections/model-bound/convention-based/no-auto-map-property.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/convention-based/no-auto-map-property.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/event-sequence-source/event-log.md b/Documentation/client-snippets/projections/model-bound/event-sequence-source/event-log.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/event-sequence-source/event-log.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/event-sequence-source/model-bound.md b/Documentation/client-snippets/projections/model-bound/event-sequence-source/model-bound.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/event-sequence-source/model-bound.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/from-all/attribute-convention.md b/Documentation/client-snippets/projections/model-bound/from-all/attribute-convention.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/from-all/attribute-convention.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/from-all/fluent-context.md b/Documentation/client-snippets/projections/model-bound/from-all/fluent-context.md new file mode 100644 index 0000000..d6ce99c --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/from-all/fluent-context.md @@ -0,0 +1,31 @@ +```typescript +import { eventType, Guid, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class InventoryRegisteredFromAll { + constructor(readonly productName: string) {} +} + +@eventType() +class InventoryAdjustedFromAll { + constructor(readonly quantity: number) {} +} + +class InventoryStatusFromAll { + id: Guid = Guid.empty; + productName = ''; + lastUpdated = new Date(); +} + +@projection() +class InventoryStatusFromAllProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(InventoryRegisteredFromAll) + .from(InventoryAdjustedFromAll) + .fromEvery(_ => _ + .set(m => m.lastUpdated) + .toEventContextProperty('occurred')); + } +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/nested/basic-lifecycle.md b/Documentation/client-snippets/projections/model-bound/nested/basic-lifecycle.md new file mode 100644 index 0000000..e5d195d --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/nested/basic-lifecycle.md @@ -0,0 +1,29 @@ +```typescript +import { clearWith, eventType, fromEvent, Guid, nested, readModel } from '@cratis/chronicle'; + +@eventType() +class CommandSetForNestedBasic { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class CommandClearedForNestedBasic { +} + +@fromEvent(CommandSetForNestedBasic) +@clearWith(CommandClearedForNestedBasic) +class CommandItemNestedBasic { + name = ''; + schema = ''; +} + +@readModel() +@fromEvent(CommandSetForNestedBasic) +class SliceWithNestedCommandBasic { + id: Guid = Guid.empty; + name = ''; + + @nested + command: CommandItemNestedBasic | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/nested/clear-with.md b/Documentation/client-snippets/projections/model-bound/nested/clear-with.md new file mode 100644 index 0000000..daa800d --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/nested/clear-with.md @@ -0,0 +1,19 @@ +```typescript +import { clearWith, eventType, fromEvent } from '@cratis/chronicle'; + +@eventType() +class CommandSetForNestedClear { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class CommandClearedForNestedClear { +} + +@fromEvent(CommandSetForNestedClear) +@clearWith(CommandClearedForNestedClear) +class CommandItemNestedClear { + name = ''; + schema = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/nested/complete.md b/Documentation/client-snippets/projections/model-bound/nested/complete.md new file mode 100644 index 0000000..c40b98d --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/nested/complete.md @@ -0,0 +1,73 @@ +```typescript +import { clearWith, eventType, fromEvent, Guid, nested, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class SliceCreatedForNestedComplete { + constructor(readonly name: string) {} +} + +@eventType() +class CommandSetForNestedComplete { + constructor( + readonly commandId: Guid, + readonly name: string, + readonly schema: string, + readonly rules: string, + readonly stateSchema: string + ) {} +} + +@eventType() +class CommandRenamedForNestedComplete { + constructor(readonly commandId: Guid, readonly name: string) {} +} + +@eventType() +class CommandDefinitionUpdatedForNestedComplete { + constructor( + readonly commandId: Guid, + readonly schema: string, + readonly rules: string, + readonly stateSchema: string + ) {} +} + +@eventType() +class CommandClearedForNestedComplete { +} + +@fromEvent(CommandSetForNestedComplete) +@fromEvent(CommandRenamedForNestedComplete) +@fromEvent(CommandDefinitionUpdatedForNestedComplete) +@clearWith(CommandClearedForNestedComplete) +class CommandItemNestedComplete { + @setFrom(CommandSetForNestedComplete, 'commandId') + id: Guid = Guid.empty; + + @setFrom(CommandSetForNestedComplete, 'name') + @setFrom(CommandRenamedForNestedComplete, 'name') + name = ''; + + @setFrom(CommandSetForNestedComplete, 'schema') + @setFrom(CommandDefinitionUpdatedForNestedComplete, 'schema') + schema = ''; + + @setFrom(CommandSetForNestedComplete, 'rules') + @setFrom(CommandDefinitionUpdatedForNestedComplete, 'rules') + rules = ''; + + @setFrom(CommandSetForNestedComplete, 'stateSchema') + @setFrom(CommandDefinitionUpdatedForNestedComplete, 'stateSchema') + stateSchema = ''; +} + +@readModel() +@fromEvent(SliceCreatedForNestedComplete) +class SliceNestedComplete { + id: Guid = Guid.empty; + name = ''; + + @nested + command: CommandItemNestedComplete | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/nested/explicit-mapping.md b/Documentation/client-snippets/projections/model-bound/nested/explicit-mapping.md new file mode 100644 index 0000000..1239a6e --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/nested/explicit-mapping.md @@ -0,0 +1,29 @@ +```typescript +import { clearWith, eventType, fromEvent, setFrom } from '@cratis/chronicle'; + +@eventType() +class CommandSetForNestedExplicit { + constructor(readonly commandName: string, readonly jsonSchema: string) {} +} + +@eventType() +class CommandSchemaUpdatedForNestedExplicit { + constructor(readonly updatedSchema: string) {} +} + +@eventType() +class CommandClearedForNestedExplicit { +} + +@fromEvent(CommandSetForNestedExplicit) +@fromEvent(CommandSchemaUpdatedForNestedExplicit) +@clearWith(CommandClearedForNestedExplicit) +class CommandItemNestedExplicit { + @setFrom(CommandSetForNestedExplicit, 'commandName') + name = ''; + + @setFrom(CommandSetForNestedExplicit, 'jsonSchema') + @setFrom(CommandSchemaUpdatedForNestedExplicit, 'updatedSchema') + schema = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/nested/multiple-clear-events.md b/Documentation/client-snippets/projections/model-bound/nested/multiple-clear-events.md new file mode 100644 index 0000000..c97fb3a --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/nested/multiple-clear-events.md @@ -0,0 +1,24 @@ +```typescript +import { clearWith, eventType, fromEvent } from '@cratis/chronicle'; + +@eventType() +class CommandSetForNestedMultipleClear { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class CommandClearedForNestedMultipleClear { +} + +@eventType() +class SliceArchivedForNestedMultipleClear { +} + +@fromEvent(CommandSetForNestedMultipleClear) +@clearWith(CommandClearedForNestedMultipleClear) +@clearWith(SliceArchivedForNestedMultipleClear) +class CommandItemNestedMultipleClear { + name = ''; + schema = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/nested/multiple-from-events.md b/Documentation/client-snippets/projections/model-bound/nested/multiple-from-events.md new file mode 100644 index 0000000..ca53ad0 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/nested/multiple-from-events.md @@ -0,0 +1,31 @@ +```typescript +import { clearWith, eventType, fromEvent } from '@cratis/chronicle'; + +@eventType() +class CommandSetForNestedMultipleFrom { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class CommandRenamedForNestedMultipleFrom { + constructor(readonly name: string) {} +} + +@eventType() +class CommandSchemaUpdatedForNestedMultipleFrom { + constructor(readonly schema: string) {} +} + +@eventType() +class CommandClearedForNestedMultipleFrom { +} + +@fromEvent(CommandSetForNestedMultipleFrom) +@fromEvent(CommandRenamedForNestedMultipleFrom) +@fromEvent(CommandSchemaUpdatedForNestedMultipleFrom) +@clearWith(CommandClearedForNestedMultipleFrom) +class CommandItemNestedMultipleFrom { + name = ''; + schema = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/nested/multiple-nested.md b/Documentation/client-snippets/projections/model-bound/nested/multiple-nested.md new file mode 100644 index 0000000..42ade05 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/nested/multiple-nested.md @@ -0,0 +1,45 @@ +```typescript +import { clearWith, eventType, fromEvent, nested } from '@cratis/chronicle'; + +@eventType() +class CommandSetForNestedMultiple { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class CommandClearedForNestedMultiple { +} + +@eventType() +class ValidationConfiguredForNestedMultiple { + constructor(readonly rules: string, readonly isStrict: boolean) {} +} + +@eventType() +class ValidationRemovedForNestedMultiple { +} + +@fromEvent(CommandSetForNestedMultiple) +@clearWith(CommandClearedForNestedMultiple) +class CommandItemNestedMultiple { + name = ''; + schema = ''; +} + +@fromEvent(ValidationConfiguredForNestedMultiple) +@clearWith(ValidationRemovedForNestedMultiple) +class ValidationConfigNestedMultiple { + rules = ''; + isStrict = false; +} + +class SliceWithMultipleNestedObjects { + name = ''; + + @nested + command: CommandItemNestedMultiple | null = null; + + @nested + validation: ValidationConfigNestedMultiple | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/nested/nested-in-children.md b/Documentation/client-snippets/projections/model-bound/nested/nested-in-children.md new file mode 100644 index 0000000..0c1853a --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/nested/nested-in-children.md @@ -0,0 +1,41 @@ +```typescript +import { childrenFrom, clearWith, eventType, fromEvent, Guid, nested } from '@cratis/chronicle'; + +@eventType() +class TaskAddedForNestedChildren { + constructor(readonly taskId: Guid, readonly title: string) {} +} + +@eventType() +class TaskAssignedForNestedChildren { + constructor(readonly taskId: Guid, readonly name: string, readonly email: string) {} +} + +@eventType() +class TaskUnassignedForNestedChildren { + constructor(readonly taskId: Guid) {} +} + +@fromEvent(TaskAssignedForNestedChildren) +@clearWith(TaskUnassignedForNestedChildren) +class TaskAssigneeNestedChild { + name = ''; + email = ''; +} + +class ProjectTaskWithNestedAssignee { + taskId: Guid = Guid.empty; + title = ''; + + @nested + assignee: TaskAssigneeNestedChild | null = null; +} + +class ProjectWithNestedChildren { + id: Guid = Guid.empty; + name = ''; + + @childrenFrom(TaskAddedForNestedChildren, 'taskId') + tasks: ProjectTaskWithNestedAssignee[] = []; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/nested/no-automap.md b/Documentation/client-snippets/projections/model-bound/nested/no-automap.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/nested/no-automap.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/model-bound/nested/parent-property.md b/Documentation/client-snippets/projections/model-bound/nested/parent-property.md new file mode 100644 index 0000000..f835722 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/nested/parent-property.md @@ -0,0 +1,13 @@ +```typescript +import { nested } from '@cratis/chronicle'; + +class NestedPropertyChild { + name = ''; + description = ''; +} + +class ParentWithNestedProperty { + @nested + child: NestedPropertyChild | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/basic.md b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/custom-sequence.md b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/custom-sequence.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/custom-sequence.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/error-handling.md b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/error-handling.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/error-handling.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/inferred-vs-explicit.md b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/inferred-vs-explicit.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/inferred-vs-explicit.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/type-mismatch.md b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/type-mismatch.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/type-mismatch.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/tagging-projections/mixed-approach.md b/Documentation/client-snippets/projections/tagging-projections/mixed-approach.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/tagging-projections/mixed-approach.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/tagging-projections/model-bound.md b/Documentation/client-snippets/projections/tagging-projections/model-bound.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/tagging-projections/model-bound.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/tagging-projections/multiple-tags-multiple-attributes.md b/Documentation/client-snippets/projections/tagging-projections/multiple-tags-multiple-attributes.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/tagging-projections/multiple-tags-multiple-attributes.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/tagging-projections/multiple-tags-single-attribute.md b/Documentation/client-snippets/projections/tagging-projections/multiple-tags-single-attribute.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/tagging-projections/multiple-tags-single-attribute.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/tagging-projections/single-tag.md b/Documentation/client-snippets/projections/tagging-projections/single-tag.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/tagging-projections/single-tag.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/tagging-projections/tag-categories.md b/Documentation/client-snippets/projections/tagging-projections/tag-categories.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/tagging-projections/tag-categories.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 2477b7d13f817f622e4de15e5fa7ccceb2ab84d8 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 11:45:55 +0200 Subject: [PATCH 30/39] Add TypeScript snippets for contributing/kernel docs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017gNGagYJgxyTSwGg5HmNVR --- .../contributing/kernel/contracts/data-contract.md | 3 +++ .../contributing/kernel/contracts/datetimeoffset-usage.md | 3 +++ .../contributing/kernel/contracts/oneof-message.md | 3 +++ .../contributing/kernel/contracts/oneof-service.md | 3 +++ .../contributing/kernel/contracts/oneof-usage.md | 3 +++ .../kernel/contracts/serializable-datetimeoffset.md | 3 +++ .../contributing/kernel/contracts/service-contract.md | 3 +++ .../client-snippets/contributing/kernel/contracts/streaming.md | 3 +++ .../contributing/kernel/patches/index/basic-structure.md | 3 +++ .../contributing/kernel/patches/index/dependencies.md | 3 +++ .../contributing/kernel/patches/index/idempotency.md | 3 +++ .../contributing/kernel/patches/index/implement-down.md | 3 +++ .../kernel/patches/index/rename-reactors-example.md | 3 +++ .../contributing/kernel/patches/index/semantic-logging.md | 3 +++ .../client-snippets/contributing/kernel/patches/index/spec.md | 3 +++ 15 files changed, 45 insertions(+) create mode 100644 Documentation/client-snippets/contributing/kernel/contracts/data-contract.md create mode 100644 Documentation/client-snippets/contributing/kernel/contracts/datetimeoffset-usage.md create mode 100644 Documentation/client-snippets/contributing/kernel/contracts/oneof-message.md create mode 100644 Documentation/client-snippets/contributing/kernel/contracts/oneof-service.md create mode 100644 Documentation/client-snippets/contributing/kernel/contracts/oneof-usage.md create mode 100644 Documentation/client-snippets/contributing/kernel/contracts/serializable-datetimeoffset.md create mode 100644 Documentation/client-snippets/contributing/kernel/contracts/service-contract.md create mode 100644 Documentation/client-snippets/contributing/kernel/contracts/streaming.md create mode 100644 Documentation/client-snippets/contributing/kernel/patches/index/basic-structure.md create mode 100644 Documentation/client-snippets/contributing/kernel/patches/index/dependencies.md create mode 100644 Documentation/client-snippets/contributing/kernel/patches/index/idempotency.md create mode 100644 Documentation/client-snippets/contributing/kernel/patches/index/implement-down.md create mode 100644 Documentation/client-snippets/contributing/kernel/patches/index/rename-reactors-example.md create mode 100644 Documentation/client-snippets/contributing/kernel/patches/index/semantic-logging.md create mode 100644 Documentation/client-snippets/contributing/kernel/patches/index/spec.md diff --git a/Documentation/client-snippets/contributing/kernel/contracts/data-contract.md b/Documentation/client-snippets/contributing/kernel/contracts/data-contract.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/contracts/data-contract.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/contracts/datetimeoffset-usage.md b/Documentation/client-snippets/contributing/kernel/contracts/datetimeoffset-usage.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/contracts/datetimeoffset-usage.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/contracts/oneof-message.md b/Documentation/client-snippets/contributing/kernel/contracts/oneof-message.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/contracts/oneof-message.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/contracts/oneof-service.md b/Documentation/client-snippets/contributing/kernel/contracts/oneof-service.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/contracts/oneof-service.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/contracts/oneof-usage.md b/Documentation/client-snippets/contributing/kernel/contracts/oneof-usage.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/contracts/oneof-usage.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/contracts/serializable-datetimeoffset.md b/Documentation/client-snippets/contributing/kernel/contracts/serializable-datetimeoffset.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/contracts/serializable-datetimeoffset.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/contracts/service-contract.md b/Documentation/client-snippets/contributing/kernel/contracts/service-contract.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/contracts/service-contract.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/contracts/streaming.md b/Documentation/client-snippets/contributing/kernel/contracts/streaming.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/contracts/streaming.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/patches/index/basic-structure.md b/Documentation/client-snippets/contributing/kernel/patches/index/basic-structure.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/patches/index/basic-structure.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/patches/index/dependencies.md b/Documentation/client-snippets/contributing/kernel/patches/index/dependencies.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/patches/index/dependencies.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/patches/index/idempotency.md b/Documentation/client-snippets/contributing/kernel/patches/index/idempotency.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/patches/index/idempotency.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/patches/index/implement-down.md b/Documentation/client-snippets/contributing/kernel/patches/index/implement-down.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/patches/index/implement-down.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/patches/index/rename-reactors-example.md b/Documentation/client-snippets/contributing/kernel/patches/index/rename-reactors-example.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/patches/index/rename-reactors-example.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/patches/index/semantic-logging.md b/Documentation/client-snippets/contributing/kernel/patches/index/semantic-logging.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/patches/index/semantic-logging.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/kernel/patches/index/spec.md b/Documentation/client-snippets/contributing/kernel/patches/index/spec.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/contributing/kernel/patches/index/spec.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` From 88f44a2cec3cde6338f13599b4c2f059d86d2bd9 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 13:28:51 +0200 Subject: [PATCH 31/39] Add @subject() decorator for compliance subject resolution Mirrors the .NET client's SubjectAttribute: a property decorator that marks the natural person a read model's PII belongs to, so a future resolver can select the correct encryption key instead of assuming a hardcoded 'id' property. --- Source/compliance/index.ts | 1 + Source/compliance/subject.spec.ts | 45 ++++++++++++++++++++++ Source/compliance/subject.ts | 64 +++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 Source/compliance/subject.spec.ts create mode 100644 Source/compliance/subject.ts diff --git a/Source/compliance/index.ts b/Source/compliance/index.ts index 582e833..b53ece5 100644 --- a/Source/compliance/index.ts +++ b/Source/compliance/index.ts @@ -4,6 +4,7 @@ export { ComplianceMetadataType } from './ComplianceMetadataType'; export type { ComplianceMetadata } from './ComplianceMetadata'; export { pii, getPIIMetadata, hasPIIMetadata, getTypePIIMetadata, isPII } from './pii'; +export { subject, hasSubjectMetadata, getSubjectPropertyName } from './subject'; export { PIINotSupportedOnEventSourceId } from './PIINotSupportedOnEventSourceId'; export { ComplianceMetadataResolver } from './ComplianceMetadataResolver'; export type { IPIIManager } from './IPIIManager'; diff --git a/Source/compliance/subject.spec.ts b/Source/compliance/subject.spec.ts new file mode 100644 index 0000000..1116499 --- /dev/null +++ b/Source/compliance/subject.spec.ts @@ -0,0 +1,45 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { describe, expect, it } from 'vitest'; +import { getSubjectPropertyName, hasSubjectMetadata, subject } from './subject'; + +// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these +// fixtures don't depend on the test runner's decorator-syntax support. + +describe('subject', () => { + describe('when applied to a property', () => { + class SomeReadModel { + personId = ''; + name = ''; + } + subject()(SomeReadModel.prototype, 'personId'); + + it('should mark the decorated property with subject metadata', () => { + expect(hasSubjectMetadata(SomeReadModel.prototype, 'personId')).toBe(true); + }); + + it('should leave other properties without subject metadata', () => { + expect(hasSubjectMetadata(SomeReadModel.prototype, 'name')).toBe(false); + }); + + it('should record the decorated property name on the declaring type', () => { + expect(getSubjectPropertyName(SomeReadModel)).toBe('personId'); + }); + }); + + describe('when no property is decorated', () => { + class PlainReadModel { + id = ''; + } + + it('should not record a subject property on the declaring type', () => { + expect(getSubjectPropertyName(PlainReadModel)).toBeUndefined(); + }); + + it('should report no subject metadata for its properties', () => { + expect(hasSubjectMetadata(PlainReadModel.prototype, 'id')).toBe(false); + }); + }); +}); diff --git a/Source/compliance/subject.ts b/Source/compliance/subject.ts new file mode 100644 index 0000000..4be24c7 --- /dev/null +++ b/Source/compliance/subject.ts @@ -0,0 +1,64 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { TypeIntrospector } from '../types'; + +/** Metadata key for the subject decorator on a property. */ +const SUBJECT_PROPERTY_METADATA_KEY = 'chronicle:compliance:subject:property'; + +/** Metadata key for the name of the subject property recorded on the declaring type. */ +const SUBJECT_TYPE_METADATA_KEY = 'chronicle:compliance:subject:type'; + +/** + * Decorator that marks a property as the compliance subject - the natural person whose Personal + * Identifiable Information (PII) a read model or event carries. The subject selects which + * encryption key protects that PII, and which key a manual release operation must use. + * + * Mirrors the .NET client's `SubjectAttribute`. When no property is decorated, resolvers fall + * back to the `id` property by convention, so read models that predate this decorator keep + * working unchanged. + * + * @returns A property decorator. + * + * @example + * ```typescript + * @readModel() + * class Employee { + * @subject() + * personId: string = ''; + * + * @pii('Employee social security number') + * ssn: string = ''; + * } + * ``` + */ +export function subject(): PropertyDecorator { + return (target: object, propertyKey: string | symbol) => { + const key = propertyKey.toString(); + const declaringType = (target as { constructor: Function }).constructor; + + TypeIntrospector.trackProperty(declaringType, key); + Reflect.defineMetadata(SUBJECT_PROPERTY_METADATA_KEY, true, target, key); + Reflect.defineMetadata(SUBJECT_TYPE_METADATA_KEY, key, declaringType); + }; +} + +/** + * Checks whether a property has been decorated with @subject. + * @param target - The class prototype. + * @param propertyKey - The property name. + * @returns True if the property has the @subject decorator; false otherwise. + */ +export function hasSubjectMetadata(target: object, propertyKey: string): boolean { + return Reflect.hasMetadata(SUBJECT_PROPERTY_METADATA_KEY, target, propertyKey); +} + +/** + * Gets the name of the property decorated with @subject on a type, if any. + * @param type - The type constructor to inspect. + * @returns The decorated property name, or undefined when no property is decorated. + */ +export function getSubjectPropertyName(type: Function): string | undefined { + return Reflect.getMetadata(SUBJECT_TYPE_METADATA_KEY, type); +} From 4e70dd43e2bdb8b78ce3d938891070457be8a57c Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 13:28:56 +0200 Subject: [PATCH 32/39] Add ReadModelSubjectResolver for read-model subject lookup Given a read model type and instance, resolves the compliance subject by checking the @subject()-decorated property first and falling back to the conventional 'id' property, mirroring the .NET client's ReadModelSubjectResolver. --- .../ReadModelSubjectResolver.spec.ts | 84 +++++++++++++++++++ Source/readModels/ReadModelSubjectResolver.ts | 44 ++++++++++ Source/readModels/index.ts | 1 + 3 files changed, 129 insertions(+) create mode 100644 Source/readModels/ReadModelSubjectResolver.spec.ts create mode 100644 Source/readModels/ReadModelSubjectResolver.ts diff --git a/Source/readModels/ReadModelSubjectResolver.spec.ts b/Source/readModels/ReadModelSubjectResolver.spec.ts new file mode 100644 index 0000000..d220551 --- /dev/null +++ b/Source/readModels/ReadModelSubjectResolver.spec.ts @@ -0,0 +1,84 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { describe, expect, it } from 'vitest'; +import { subject } from '../compliance/subject'; +import { ReadModelSubjectResolver } from './ReadModelSubjectResolver'; + +// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these +// fixtures don't depend on the test runner's decorator-syntax support. + +describe('ReadModelSubjectResolver', () => { + describe('when the read model has a property decorated with @subject()', () => { + class Employee { + id = ''; + personId = ''; + } + subject()(Employee.prototype, 'personId'); + + it('should resolve to the decorated property value', () => { + const instance = new Employee(); + instance.id = 'employee-1'; + instance.personId = 'person-42'; + + expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).toBe('person-42'); + }); + + it('should take precedence over the id property', () => { + const instance = new Employee(); + instance.id = 'employee-1'; + instance.personId = 'person-42'; + + expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).not.toBe('employee-1'); + }); + + it('should fall back to id when the decorated property has no value', () => { + const instance = new Employee(); + instance.id = 'employee-1'; + instance.personId = ''; + + expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).toBe('employee-1'); + }); + }); + + describe('when the read model has no property decorated with @subject()', () => { + class Customer { + id = ''; + name = ''; + } + + it('should fall back to the id property', () => { + const instance = new Customer(); + instance.id = 'customer-7'; + + expect(ReadModelSubjectResolver.resolveFrom(Customer, instance)).toBe('customer-7'); + }); + }); + + describe('when the read model has neither a decorated property nor an id property', () => { + class Anonymous { + name = ''; + } + + it('should not resolve a subject', () => { + const instance = new Anonymous(); + + expect(ReadModelSubjectResolver.resolveFrom(Anonymous, instance)).toBeUndefined(); + }); + }); + + describe('when the instance does not exist', () => { + class Customer { + id = ''; + } + + it('should not resolve a subject for null', () => { + expect(ReadModelSubjectResolver.resolveFrom(Customer, null as unknown as Customer)).toBeUndefined(); + }); + + it('should not resolve a subject for undefined', () => { + expect(ReadModelSubjectResolver.resolveFrom(Customer, undefined as unknown as Customer)).toBeUndefined(); + }); + }); +}); diff --git a/Source/readModels/ReadModelSubjectResolver.ts b/Source/readModels/ReadModelSubjectResolver.ts new file mode 100644 index 0000000..03493a6 --- /dev/null +++ b/Source/readModels/ReadModelSubjectResolver.ts @@ -0,0 +1,44 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import type { Constructor } from '@cratis/fundamentals'; +import { getSubjectPropertyName } from '../compliance/subject'; + +/** + * Resolves the compliance subject - the natural person a read model's Personal Identifiable + * Information (PII) belongs to - from a read model instance. + */ +export class ReadModelSubjectResolver { + /** + * Attempts to derive the compliance subject from a read model instance. + * + * Resolution order: + * 1. The property decorated with `@subject()` on {@link readModelType}, when it has a value. + * 2. The `id` property, by convention - kept so read models that predate `@subject()` + * continue to resolve exactly as before. + * @param readModelType - The read model type to resolve subject metadata for. + * @param instance - The read model instance to inspect, or undefined/null for a read model + * that does not exist. + * @returns The resolved subject, or undefined when neither source yields a value. + */ + static resolveFrom(readModelType: Constructor, instance: TReadModel): string | undefined { + if (instance === undefined || instance === null) { + return undefined; + } + + const anyInstance = instance as Record; + const subjectProperty = getSubjectPropertyName(readModelType); + const explicitSubject = subjectProperty ? ReadModelSubjectResolver.toSubject(anyInstance[subjectProperty]) : undefined; + + return explicitSubject ?? ReadModelSubjectResolver.toSubject(anyInstance.id); + } + + private static toSubject(value: unknown): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + const stringValue = String(value); + return stringValue.length > 0 ? stringValue : undefined; + } +} diff --git a/Source/readModels/index.ts b/Source/readModels/index.ts index 368249a..70cf548 100644 --- a/Source/readModels/index.ts +++ b/Source/readModels/index.ts @@ -6,6 +6,7 @@ export type { IReadModels } from './IReadModels'; export type { IMaterializedReadModels } from './IMaterializedReadModels'; export { ReadModels } from './ReadModels'; export { MaterializedReadModels } from './MaterializedReadModels'; +export { ReadModelSubjectResolver } from './ReadModelSubjectResolver'; export type { ReadModelChangeset } from './ReadModelChangeset'; export type { ReadModelSnapshot } from './ReadModelSnapshot'; export { readModel, getReadModelMetadata, isReadModel } from './readModel'; From b3ce70f64f7790fd97d5e1d67484f7f4ab9f3676 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 13:29:03 +0200 Subject: [PATCH 33/39] Replace hardcoded 'id' subject lookups with ReadModelSubjectResolver ReadModels.release/releaseMany used to throw unless a read model had a property literally named 'id'. MaterializedReadModels silently skipped release the same way. Both now defer to ReadModelSubjectResolver, which checks a @subject()-decorated property before falling back to 'id', so a read model whose GDPR subject isn't named 'id' releases against the correct encryption key. The 'id' fallback keeps existing read models working unchanged. --- .../readModels/MaterializedReadModels.spec.ts | 98 +++++++++++++++ Source/readModels/MaterializedReadModels.ts | 10 +- Source/readModels/ReadModels.spec.ts | 118 ++++++++++++++++++ Source/readModels/ReadModels.ts | 14 +-- 4 files changed, 225 insertions(+), 15 deletions(-) create mode 100644 Source/readModels/MaterializedReadModels.spec.ts create mode 100644 Source/readModels/ReadModels.spec.ts diff --git a/Source/readModels/MaterializedReadModels.spec.ts b/Source/readModels/MaterializedReadModels.spec.ts new file mode 100644 index 0000000..30e0667 --- /dev/null +++ b/Source/readModels/MaterializedReadModels.spec.ts @@ -0,0 +1,98 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { field } from '@cratis/fundamentals'; +import { describe, expect, it, vi } from 'vitest'; +import type { ChronicleConnection } from '../connection'; +import { pii } from '../compliance/pii'; +import { subject } from '../compliance/subject'; +import { readModel } from './readModel'; +import { MaterializedReadModels } from './MaterializedReadModels'; + +// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these +// fixtures don't depend on the test runner's decorator-syntax support. + +function createMaterializedReadModels(instancesJson: string[], releaseResponse: Record = { HasError: false, Payload: '{}' }) { + const release = vi.fn().mockResolvedValue(releaseResponse); + const getInstances = vi.fn().mockResolvedValue({ Instances: instancesJson }); + const connection = { + compliance: { release }, + materializedReadModels: { getInstances } + } as unknown as ChronicleConnection; + + const readModels = new MaterializedReadModels('test-store', 'test-namespace', connection); + return { readModels, release }; +} + +describe('MaterializedReadModels', () => { + describe('when a read model has a property decorated with @subject()', () => { + class Employee { + id = ''; + personId = ''; + ssn = ''; + } + field(String)(Employee.prototype, 'id'); + field(String)(Employee.prototype, 'personId'); + field(String)(Employee.prototype, 'ssn'); + pii()(Employee.prototype, 'ssn'); + subject()(Employee.prototype, 'personId'); + readModel('EmployeeWithSubjectMaterialized')(Employee); + + it('should release using the decorated property as the subject', async () => { + const json = JSON.stringify({ id: 'employee-1', personId: 'person-42', ssn: '123-45-6789' }); + const { readModels, release } = createMaterializedReadModels([json]); + + await readModels.getInstances(Employee); + + expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'person-42' })); + }); + }); + + describe('when a read model has no property decorated with @subject() but has an id property', () => { + class Customer { + id = ''; + ssn = ''; + } + field(String)(Customer.prototype, 'id'); + field(String)(Customer.prototype, 'ssn'); + pii()(Customer.prototype, 'ssn'); + readModel('CustomerWithIdOnlyMaterialized')(Customer); + + it('should fall back to the id property as the subject, unchanged from today', async () => { + const json = JSON.stringify({ id: 'customer-7', ssn: '987-65-4321' }); + const { readModels, release } = createMaterializedReadModels([json]); + + await readModels.getInstances(Customer); + + expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'customer-7' })); + }); + }); + + describe('when a read model has neither a decorated property nor an id property', () => { + class Anonymous { + ssn = ''; + } + field(String)(Anonymous.prototype, 'ssn'); + pii()(Anonymous.prototype, 'ssn'); + readModel('AnonymousMaterialized')(Anonymous); + + it('should not call release, unchanged from today', async () => { + const json = JSON.stringify({ ssn: '000-00-0000' }); + const { readModels, release } = createMaterializedReadModels([json]); + + await readModels.getInstances(Anonymous); + + expect(release).not.toHaveBeenCalled(); + }); + + it('should return the instance unreleased', async () => { + const json = JSON.stringify({ ssn: '000-00-0000' }); + const { readModels } = createMaterializedReadModels([json]); + + const [instance] = await readModels.getInstances(Anonymous); + + expect(instance.ssn).toBe('000-00-0000'); + }); + }); +}); diff --git a/Source/readModels/MaterializedReadModels.ts b/Source/readModels/MaterializedReadModels.ts index 1096b0e..9238e71 100644 --- a/Source/readModels/MaterializedReadModels.ts +++ b/Source/readModels/MaterializedReadModels.ts @@ -6,6 +6,7 @@ import { JsonSerializer } from '@cratis/fundamentals'; import { ChronicleConnection } from '../connection'; import { JsonSchemaGenerator } from '../schemas'; import { getReadModelMetadata } from './readModel'; +import { ReadModelSubjectResolver } from './ReadModelSubjectResolver'; import type { IMaterializedReadModels } from './IMaterializedReadModels'; const defaultTake = 50; @@ -97,7 +98,7 @@ export class MaterializedReadModels implements IMaterializedReadModels { } private async releaseInstance(readModelType: Constructor, instance: TReadModel, schema: string): Promise { - const subject = this.extractSubject(instance); + const subject = ReadModelSubjectResolver.resolveFrom(readModelType, instance); if (!subject) { return instance; } @@ -141,11 +142,4 @@ export class MaterializedReadModels implements IMaterializedReadModels { } } - private extractSubject(instance: TReadModel): string | undefined { - const anyInstance = instance as Record; - if (anyInstance.id !== undefined && anyInstance.id !== null) { - return String(anyInstance.id); - } - return undefined; - } } diff --git a/Source/readModels/ReadModels.spec.ts b/Source/readModels/ReadModels.spec.ts new file mode 100644 index 0000000..74aa24d --- /dev/null +++ b/Source/readModels/ReadModels.spec.ts @@ -0,0 +1,118 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import type { Constructor } from '@cratis/fundamentals'; +import { describe, expect, it, vi } from 'vitest'; +import type { IClientArtifactsProvider } from '../artifacts'; +import type { ChronicleConnection } from '../connection'; +import { subject } from '../compliance/subject'; +import { fromEvent } from '../projections/modelBound/fromEvent'; +import { readModel } from './readModel'; +import { ReadModels } from './ReadModels'; + +// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these +// fixtures don't depend on the test runner's decorator-syntax support. + +class SomeEvent { + value = ''; +} + +function createReadModels(readModelType: Constructor, releaseResponse: Record = { HasError: false, Payload: '{}' }) { + const release = vi.fn().mockResolvedValue(releaseResponse); + const connection = { + compliance: { release } + } as unknown as ChronicleConnection; + + const clientArtifacts = { + eventTypes: [], + readModels: [readModelType], + reactors: [], + reducers: [], + seeders: [], + constraints: [], + projections: [], + webhooks: [], + eventTypeMigrations: [] + } as IClientArtifactsProvider; + + const readModels = new ReadModels('test-store', 'test-namespace', connection, clientArtifacts, 'default-sink'); + return { readModels, release }; +} + +describe('ReadModels', () => { + describe('when releasing a read model with a property decorated with @subject()', () => { + class Employee { + id = ''; + personId = ''; + } + subject()(Employee.prototype, 'personId'); + fromEvent(SomeEvent)(Employee); + readModel('EmployeeWithSubject')(Employee); + + it('should release using the decorated property as the subject', async () => { + const { readModels, release } = createReadModels(Employee); + const instance = new Employee(); + instance.id = 'employee-1'; + instance.personId = 'person-42'; + + await readModels.release(Employee, instance); + + expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'person-42' })); + }); + }); + + describe('when releasing a read model without @subject() but with an id property', () => { + class Customer { + id = ''; + } + fromEvent(SomeEvent)(Customer); + readModel('CustomerWithIdOnly')(Customer); + + it('should fall back to the id property as the subject, unchanged from today', async () => { + const { readModels, release } = createReadModels(Customer); + const instance = new Customer(); + instance.id = 'customer-7'; + + await readModels.release(Customer, instance); + + expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'customer-7' })); + }); + }); + + describe('when releasing a read model with neither @subject() nor an id property', () => { + class Anonymous { + name = ''; + } + fromEvent(SomeEvent)(Anonymous); + readModel('AnonymousReadModel')(Anonymous); + + it('should throw, same as today', async () => { + const { readModels } = createReadModels(Anonymous); + const instance = new Anonymous(); + + await expect(readModels.release(Anonymous, instance)).rejects.toThrow(/subject/); + }); + }); + + describe('when releasing many read model instances', () => { + class Customer { + id = ''; + } + fromEvent(SomeEvent)(Customer); + readModel('CustomerForReleaseMany')(Customer); + + it('should release each instance using its own resolved subject', async () => { + const { readModels, release } = createReadModels(Customer); + const first = new Customer(); + first.id = 'customer-1'; + const second = new Customer(); + second.id = 'customer-2'; + + await readModels.releaseMany(Customer, [first, second]); + + expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'customer-1' })); + expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'customer-2' })); + }); + }); +}); diff --git a/Source/readModels/ReadModels.ts b/Source/readModels/ReadModels.ts index 2b7e4ab..c116327 100644 --- a/Source/readModels/ReadModels.ts +++ b/Source/readModels/ReadModels.ts @@ -23,6 +23,7 @@ import { WellKnownSinks } from '../sinks'; import { getReadModelMetadata } from './readModel'; import type { IMaterializedReadModels } from './IMaterializedReadModels'; import { MaterializedReadModels } from './MaterializedReadModels'; +import { ReadModelSubjectResolver } from './ReadModelSubjectResolver'; import type { IReadModels } from './IReadModels'; import type { ReadModelChangeset } from './ReadModelChangeset'; import type { ReadModelSnapshot } from './ReadModelSnapshot'; @@ -173,7 +174,7 @@ export class ReadModels implements IReadModels { const readModel = this.resolveReadModel(readModelType); const schema = this.getReadModelSchema(readModelType, readModel.identifier); const payload = JsonSerializer.serialize(instance); - const subject = this.extractSubject(instance); + const subject = this.extractSubject(readModelType, instance); const response = await this._connection.compliance.release({ EventStore: this._eventStore, @@ -345,12 +346,11 @@ export class ReadModels implements IReadModels { } } - private extractSubject(instance: TReadModel): string { - // By convention, use the 'id' property as the subject - const anyInstance = instance as any; - if (anyInstance.id !== undefined && anyInstance.id !== null) { - return String(anyInstance.id); + private extractSubject(readModelType: Constructor, instance: TReadModel): string { + const subject = ReadModelSubjectResolver.resolveFrom(readModelType, instance); + if (subject !== undefined) { + return subject; } - throw new Error('Read model instance must have an "id" property to serve as the subject for PII release'); + throw new Error('Read model instance must have a property decorated with @subject() or an "id" property to serve as the subject for PII release'); } } From 3bc971ab1739f802ae96051eafc862477ffa2bd4 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 13:31:26 +0200 Subject: [PATCH 34/39] Demonstrate @subject() in the releasing-PII TypeScript snippet The TypeScript snippet for IReadModels.Release() used to release against the ticket's own id, unlike the C# tab which marks CustomerId with [Subject] to release against the customer's key instead. Bring the TypeScript example to parity now that @subject() exists. --- .../read-models/releasing-pii/read-model.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Documentation/client-snippets/read-models/releasing-pii/read-model.md b/Documentation/client-snippets/read-models/releasing-pii/read-model.md index c592308..45d508b 100644 --- a/Documentation/client-snippets/read-models/releasing-pii/read-model.md +++ b/Documentation/client-snippets/read-models/releasing-pii/read-model.md @@ -1,5 +1,5 @@ ```typescript -import { EventContext, eventType, pii, reducer } from '@cratis/chronicle'; +import { EventContext, eventType, pii, reducer, subject } from '@cratis/chronicle'; @eventType() class ReleasingPiiSupportTicketOpened { @@ -7,8 +7,12 @@ class ReleasingPiiSupportTicketOpened { } class ReleasingPiiSupportTicket { - // Read models are keyed by 'id' by convention - the same property release() uses as the subject. + // The ticket's own id identifies the ticket, not the person the PII belongs to - @subject() + // tells release() to use customerId as the encryption key's owner instead. Without it, + // release() would fall back to id. id = ''; + + @subject() customerId = ''; @pii('The name of the person who opened the ticket') From 84c77fc671bbffa7d983f399c328a9a24af1e395 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 13:40:28 +0200 Subject: [PATCH 35/39] Add IProjections operational surface TypeScript's IProjections exposed only discover() and register(), leaving no way to inspect or operate on a projection once it was running. Add the state/replay/failed-partitions/query surface that .NET's IProjections already exposes, backed by the real wire RPCs: - hasFor/hasForModel/getProjectionIdFor - resolve a discovered projection's identifier, locally from what discover() found. - getStateFor/getStateForModel - Observers.GetObserverInformation. - getFailedPartitionsForModel - reuses the existing FailedPartitions wrapper, resolving the projection identifier first. - replay/replayForModel - Observers.Replay. - query - Projections.Preview, throwing UnableToQueryProjection on a PDL parsing error. Also fixes ObserverRunningState, whose values never matched the wire enum (Suspended/Quarantined had no representation at all) and had no consumer anywhere in the codebase - getStateFor is the first. Add the eventSequence()/eventLog() model-bound decorators so getStateFor(Model) can resolve a projection's real event sequence instead of assuming event-log; resolveModelBoundMetadata() picking this up for registration follows in the next commit. --- Source/EventStore.ts | 2 +- Source/observation/ObserverRunningState.ts | 27 +- Source/observation/index.ts | 1 + Source/observation/toObserverRunningState.ts | 27 ++ Source/projections/IProjections.ts | 82 +++++- Source/projections/ProjectionQueryResult.ts | 10 + Source/projections/ProjectionState.ts | 25 ++ .../Projections.childrenAndNested.spec.ts | 2 +- .../Projections.operationalSurface.spec.ts | 257 ++++++++++++++++++ Source/projections/Projections.spec.ts | 2 +- Source/projections/Projections.ts | 161 ++++++++++- Source/projections/UnableToQueryProjection.ts | 17 ++ Source/projections/index.ts | 3 + .../projections/modelBound/eventSequence.ts | 38 +++ Source/projections/modelBound/index.ts | 1 + 15 files changed, 632 insertions(+), 23 deletions(-) create mode 100644 Source/observation/toObserverRunningState.ts create mode 100644 Source/projections/ProjectionQueryResult.ts create mode 100644 Source/projections/ProjectionState.ts create mode 100644 Source/projections/Projections.operationalSurface.spec.ts create mode 100644 Source/projections/UnableToQueryProjection.ts create mode 100644 Source/projections/modelBound/eventSequence.ts diff --git a/Source/EventStore.ts b/Source/EventStore.ts index 6731c4f..0bacf4a 100644 --- a/Source/EventStore.ts +++ b/Source/EventStore.ts @@ -90,7 +90,7 @@ export class EventStore implements IEventStore { const artifacts = DefaultClientArtifactsProvider.default; this.eventTypes = new EventTypes(name.value, _connection, artifacts); this.constraints = new Constraints(name.value, _connection, artifacts); - this.projections = new Projections(name.value, _connection, artifacts, defaultSinkTypeId); + this.projections = new Projections(name.value, namespace.value, _connection, artifacts, defaultSinkTypeId); this.reactors = new Reactors(artifacts, _connection, name.value, namespace.value, lifecycle, this.eventLog); this.reducers = new Reducers(artifacts, _connection, name.value, namespace.value, lifecycle, defaultSinkTypeId); this.readModels = new ReadModels(name.value, namespace.value, _connection, artifacts, defaultSinkTypeId); diff --git a/Source/observation/ObserverRunningState.ts b/Source/observation/ObserverRunningState.ts index b75c481..42b97ec 100644 --- a/Source/observation/ObserverRunningState.ts +++ b/Source/observation/ObserverRunningState.ts @@ -8,27 +8,18 @@ export enum ObserverRunningState { /** The observer is in an unknown state. */ Unknown = 'Unknown', - /** The observer is subscribing to the event sequence. */ - Subscribing = 'Subscribing', - - /** The observer is replaying events from the beginning. */ - Replaying = 'Replaying', - - /** The observer is resuming after a pause. */ - Resuming = 'Resuming', - - /** The observer is actively processing events. */ + /** The observer is active and waiting for new events. */ Active = 'Active', - /** The observer is paused and not processing events. */ - Paused = 'Paused', + /** The observer is suspended. */ + Suspended = 'Suspended', - /** The observer has stopped processing events. */ - Stopped = 'Stopped', + /** The observer is replaying. */ + Replaying = 'Replaying', - /** The observer is in a failed state. */ - Failed = 'Failed', + /** The observer is disconnected. */ + Disconnected = 'Disconnected', - /** The observer has been disconnected. */ - Disconnected = 'Disconnected' + /** The observer is quarantined. */ + Quarantined = 'Quarantined' } diff --git a/Source/observation/index.ts b/Source/observation/index.ts index 4522b8b..942d631 100644 --- a/Source/observation/index.ts +++ b/Source/observation/index.ts @@ -7,5 +7,6 @@ export type { FailedPartition } from './FailedPartition'; export type { FailedPartitionAttempt } from './FailedPartitionAttempt'; export type { IFailedPartitions } from './IFailedPartitions'; export { FailedPartitions } from './FailedPartitions'; +export { toObserverRunningState } from './toObserverRunningState'; export type { ICanBeNotifiedWhenReplay } from './ICanBeNotifiedWhenReplay'; export type { ICanBeNotifiedWhenPartitionReplayed } from './ICanBeNotifiedWhenPartitionReplayed'; diff --git a/Source/observation/toObserverRunningState.ts b/Source/observation/toObserverRunningState.ts new file mode 100644 index 0000000..845eb27 --- /dev/null +++ b/Source/observation/toObserverRunningState.ts @@ -0,0 +1,27 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { ObserverRunningState as ContractObserverRunningState } from '@cratis/chronicle.contracts'; +import { ObserverRunningState } from './ObserverRunningState'; + +/** + * Converts a wire {@link ContractObserverRunningState} into the client {@link ObserverRunningState}. + * @param state - The wire running state to convert. + * @returns The converted client running state. + */ +export function toObserverRunningState(state: ContractObserverRunningState): ObserverRunningState { + switch (state) { + case ContractObserverRunningState.Active: + return ObserverRunningState.Active; + case ContractObserverRunningState.Suspended: + return ObserverRunningState.Suspended; + case ContractObserverRunningState.Replaying: + return ObserverRunningState.Replaying; + case ContractObserverRunningState.Disconnected: + return ObserverRunningState.Disconnected; + case ContractObserverRunningState.Quarantined: + return ObserverRunningState.Quarantined; + default: + return ObserverRunningState.Unknown; + } +} diff --git a/Source/projections/IProjections.ts b/Source/projections/IProjections.ts index 72fd212..cb02f7a 100644 --- a/Source/projections/IProjections.ts +++ b/Source/projections/IProjections.ts @@ -1,8 +1,16 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { Constructor } from '@cratis/fundamentals'; +import { FailedPartition } from '../observation/FailedPartition'; +import { JobId } from '../jobs/JobId'; +import { ProjectionId } from './ProjectionId'; +import { ProjectionQueryResult } from './ProjectionQueryResult'; +import { ProjectionState } from './ProjectionState'; + /** - * Defines a system to work with projections, including discovery and registration with the Kernel. + * Defines a system to work with projections, including discovery, registration and operating + * on projections that are known to the Chronicle Kernel. */ export interface IProjections { /** @@ -16,4 +24,76 @@ export interface IProjections { * @returns A promise that resolves when registration is complete. */ register(): Promise; + + /** + * Check if there is a definition for a specific projection identifier. + * @param projectionId - Identifier of the projection. + * @returns True if it exists, false if not. + */ + hasFor(projectionId: ProjectionId | string): boolean; + + /** + * Check if there is a definition for the projection that maintains a specific read model. + * @param readModelType - Type of read model to check for. + * @returns True if it exists, false if not. + */ + hasForModel(readModelType: Constructor): boolean; + + /** + * Get the {@link ProjectionId} for the projection that maintains a specific read model. + * @param readModelType - Type of read model to get for. + * @returns The {@link ProjectionId} for the read model. + * @remarks A model-bound projection has no projection type of its own - its read model type is the + * only handle it has. A declarative projection is only resolvable here when it declares its + * read model type explicitly (the second argument to the `projection()` decorator); one whose read + * model is inferred at registration time cannot be resolved before `register()` has run. + */ + getProjectionIdFor(readModelType: Constructor): ProjectionId; + + /** + * Get the state of a specific projection. + * @param projectionId - Identifier of the projection to get the state for. + * @returns The {@link ProjectionState}. + */ + getStateFor(projectionId: ProjectionId | string): Promise; + + /** + * Get the state of the projection that maintains a specific read model. + * @param readModelType - Type of read model to get the state for. + * @returns The {@link ProjectionState}. + */ + getStateForModel(readModelType: Constructor): Promise; + + /** + * Get any failed partitions for the projection that maintains a specific read model. + * @param readModelType - Type of read model to get for. + * @returns Collection of {@link FailedPartition}, if any. + */ + getFailedPartitionsForModel(readModelType: Constructor): Promise; + + /** + * Replay a specific projection by its identifier. + * @param projectionId - Identifier of the projection to replay. + * @returns The {@link JobId} of the replay job that was started or resumed. + */ + replay(projectionId: ProjectionId | string): Promise; + + /** + * Replay the projection that maintains a specific read model. + * @param readModelType - Type of read model to replay the projection for. + * @returns The {@link JobId} of the replay job that was started or resumed. + */ + replayForModel(readModelType: Constructor): Promise; + + /** + * Query a projection declaration against the event log without registering it. + * @param declaration - The Projection Declaration Language string to query. + * @param eventSequenceId - Optional event sequence identifier to query. Defaults to `"event-log"`. + * @returns A {@link ProjectionQueryResult} containing the resulting read model entries. + * @remarks The declaration may omit the `=> ReadModelType` target - in that case the read model + * schema is inferred from the events used in the projection. An inferred read model can never be + * registered as a permanent projection; query-only declarations are exclusively for ad-hoc + * exploration. + */ + query(declaration: string, eventSequenceId?: string): Promise; } diff --git a/Source/projections/ProjectionQueryResult.ts b/Source/projections/ProjectionQueryResult.ts new file mode 100644 index 0000000..3086c76 --- /dev/null +++ b/Source/projections/ProjectionQueryResult.ts @@ -0,0 +1,10 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/** + * Represents the result of querying a projection against the event log. + */ +export interface ProjectionQueryResult { + /** Collection of JSON representations of the resulting read model entries. */ + readonly readModelEntries: ReadonlyArray; +} diff --git a/Source/projections/ProjectionState.ts b/Source/projections/ProjectionState.ts new file mode 100644 index 0000000..559b282 --- /dev/null +++ b/Source/projections/ProjectionState.ts @@ -0,0 +1,25 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { ObserverRunningState } from '../observation/ObserverRunningState'; +import { EventSequenceNumber } from '../eventSequences/EventSequenceNumber'; + +/** + * Represents the state of a projection. + */ +export interface ProjectionState { + /** The current running state of the projection. */ + readonly runningState: ObserverRunningState; + + /** Indicates whether the projection is subscribed to its handler. */ + readonly isSubscribed: boolean; + + /** The next event sequence number the projection will process. */ + readonly nextEventSequenceNumber: EventSequenceNumber; + + /** The last event sequence number the projection handled. */ + readonly lastHandledEventSequenceNumber: EventSequenceNumber; + + /** The current tail event sequence number of the event sequence the projection observes. */ + readonly tailEventSequenceNumber: EventSequenceNumber; +} diff --git a/Source/projections/Projections.childrenAndNested.spec.ts b/Source/projections/Projections.childrenAndNested.spec.ts index aaa252a..7bdf7d1 100644 --- a/Source/projections/Projections.childrenAndNested.spec.ts +++ b/Source/projections/Projections.childrenAndNested.spec.ts @@ -124,7 +124,7 @@ function createProjections(readModels: (new (...args: unknown[]) => unknown)[]) eventTypeMigrations: [] }; - const projections = new Projections('test-store', connection, clientArtifacts, 'test-sink'); + const projections = new Projections('test-store', 'test-namespace', connection, clientArtifacts, 'test-sink'); return { projections, registerMock }; } diff --git a/Source/projections/Projections.operationalSurface.spec.ts b/Source/projections/Projections.operationalSurface.spec.ts new file mode 100644 index 0000000..b6f5ead --- /dev/null +++ b/Source/projections/Projections.operationalSurface.spec.ts @@ -0,0 +1,257 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { describe, expect, it, vi } from 'vitest'; +import { IClientArtifactsProvider } from '../artifacts'; +import { ChronicleConnection } from '../connection'; +import { ObserverRunningState } from '../observation/ObserverRunningState'; +import { eventType } from '../events/eventTypeDecorator'; +import { readModel } from '../readModels/readModel'; +import { eventSequence } from './modelBound/eventSequence'; +import { fromEvent } from './modelBound/fromEvent'; +import { projection } from './declarative/projection'; +import type { IProjectionBuilderFor } from './declarative/IProjectionBuilderFor'; +import type { IProjectionFor } from './declarative/IProjectionFor'; +import { Projections } from './Projections'; +import { UnableToQueryProjection } from './UnableToQueryProjection'; + +// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these +// fixtures don't depend on the test runner's decorator-syntax support. + +class OpStateChanged { + value!: string; +} +eventType()(OpStateChanged); + +class OpSummary { + id!: string; + value!: string; +} +fromEvent(OpStateChanged)(OpSummary); +eventSequence('custom-op-sequence')(OpSummary); +readModel()(OpSummary); + +class UndiscoveredSummary { + id!: string; +} +readModel()(UndiscoveredSummary); + +class DeclarativeReadModel { + id!: string; +} +readModel()(DeclarativeReadModel); + +class DeclarativeSummaryProjection implements IProjectionFor { + // discover() never calls define() - only register() does - so a no-op body is sufficient here. + define(_builder: IProjectionBuilderFor): void {} +} +projection('DeclarativeSummary', DeclarativeReadModel)(DeclarativeSummaryProjection); + +function wireObserverInformation() { + return { + RunningState: 1, // Active + IsSubscribed: true, + NextEventSequenceNumber: 5n, + LastHandledEventSequenceNumber: 4n, + TailEventSequenceNumber: 4n + }; +} + +async function createDiscoveredProjections() { + const getObserverInformation = vi.fn().mockResolvedValue(wireObserverInformation()); + const replay = vi.fn().mockResolvedValue({ JobId: '11111111-1111-1111-1111-111111111111' }); + const getFailedPartitions = vi.fn().mockResolvedValue({ items: [] }); + const preview = vi.fn().mockResolvedValue({ Value0: { ReadModelEntries: ['{"id":"1"}'] } }); + const registerMock = vi.fn().mockResolvedValue(undefined); + const registerManyMock = vi.fn().mockResolvedValue(undefined); + + const connection = { + observers: { getObserverInformation, replay }, + failedPartitions: { getFailedPartitions }, + projections: { register: registerMock, preview }, + readModels: { registerMany: registerManyMock } + } as unknown as ChronicleConnection; + + const clientArtifacts: IClientArtifactsProvider = { + eventTypes: [], + readModels: [OpSummary, UndiscoveredSummary, DeclarativeReadModel] as unknown as IClientArtifactsProvider['readModels'], + reactors: [], + reducers: [], + seeders: [], + constraints: [], + projections: [DeclarativeSummaryProjection] as unknown as IClientArtifactsProvider['projections'], + webhooks: [], + eventTypeMigrations: [] + }; + + const projections = new Projections('test-store', 'test-namespace', connection, clientArtifacts, 'test-sink'); + await projections.discover(); + + return { projections, getObserverInformation, replay, getFailedPartitions, preview }; +} + +describe('Projections operational surface', () => { + describe('when checking hasFor with a discovered projection identifier', () => { + it('should return true', async () => { + const { projections } = await createDiscoveredProjections(); + expect(projections.hasFor('OpSummary')).toBe(true); + }); + }); + + describe('when checking hasFor with an unknown projection identifier', () => { + it('should return false', async () => { + const { projections } = await createDiscoveredProjections(); + expect(projections.hasFor('unknown-projection')).toBe(false); + }); + }); + + describe('when checking hasForModel for a discovered model-bound read model', () => { + it('should return true', async () => { + const { projections } = await createDiscoveredProjections(); + expect(projections.hasForModel(OpSummary)).toBe(true); + }); + }); + + describe('when checking hasForModel for an undiscovered read model', () => { + it('should return false', async () => { + const { projections } = await createDiscoveredProjections(); + expect(projections.hasForModel(UndiscoveredSummary)).toBe(false); + }); + }); + + describe('when getting the projection identifier for a model-bound read model', () => { + it('should resolve the read model identifier', async () => { + const { projections } = await createDiscoveredProjections(); + expect(projections.getProjectionIdFor(OpSummary).value).toBe('OpSummary'); + }); + }); + + describe('when getting the projection identifier for a declarative projection with an explicit read model type', () => { + it('should resolve the declared projection identifier', async () => { + const { projections } = await createDiscoveredProjections(); + expect(projections.getProjectionIdFor(DeclarativeReadModel).value).toBe('DeclarativeSummary'); + }); + }); + + describe('when getting the projection identifier for an undiscovered read model', () => { + it('should throw', async () => { + const { projections } = await createDiscoveredProjections(); + expect(() => projections.getProjectionIdFor(UndiscoveredSummary)).toThrow(); + }); + }); + + describe('when getting state for a projection by identifier', () => { + it('should call GetObserverInformation and map the response', async () => { + const { projections, getObserverInformation } = await createDiscoveredProjections(); + + const state = await projections.getStateFor('OpSummary'); + + expect(getObserverInformation).toHaveBeenCalledTimes(1); + const request = getObserverInformation.mock.calls[0][0]; + expect(request.EventStore).toEqual('test-store'); + expect(request.Namespace).toEqual('test-namespace'); + expect(request.ObserverId).toEqual('OpSummary'); + + expect(state.runningState).toBe(ObserverRunningState.Active); + expect(state.isSubscribed).toBe(true); + expect(state.nextEventSequenceNumber.value).toEqual(5n); + expect(state.lastHandledEventSequenceNumber.value).toEqual(4n); + expect(state.tailEventSequenceNumber.value).toEqual(4n); + }); + }); + + describe('when getting state for a model-bound read model with a custom event sequence', () => { + it('should resolve the identifier and pass the model-declared event sequence', async () => { + const { projections, getObserverInformation } = await createDiscoveredProjections(); + + await projections.getStateForModel(OpSummary); + + const request = getObserverInformation.mock.calls[0][0]; + expect(request.ObserverId).toEqual('OpSummary'); + expect(request.EventSequenceId).toEqual('custom-op-sequence'); + }); + }); + + describe('when getting failed partitions for a model-bound read model', () => { + it('should call GetFailedPartitions with the resolved projection identifier', async () => { + const { projections, getFailedPartitions } = await createDiscoveredProjections(); + + await projections.getFailedPartitionsForModel(OpSummary); + + expect(getFailedPartitions).toHaveBeenCalledTimes(1); + const request = getFailedPartitions.mock.calls[0][0]; + expect(request.ObserverId).toEqual('OpSummary'); + }); + }); + + describe('when replaying a projection by identifier', () => { + it('should call Replay and parse the returned job id', async () => { + const { projections, replay } = await createDiscoveredProjections(); + + const jobId = await projections.replay('OpSummary'); + + expect(replay).toHaveBeenCalledTimes(1); + const request = replay.mock.calls[0][0]; + expect(request.ObserverId).toEqual('OpSummary'); + expect(jobId.toString()).toEqual('11111111-1111-1111-1111-111111111111'); + }); + }); + + describe('when replaying a projection that is not replayable', () => { + it('should resolve to a not-set job id', async () => { + const { projections, replay } = await createDiscoveredProjections(); + replay.mockResolvedValueOnce({ JobId: '' }); + + const jobId = await projections.replay('OpSummary'); + + expect(jobId.value.toString()).toEqual('00000000-0000-0000-0000-000000000000'); + }); + }); + + describe('when replaying a model-bound read model', () => { + it('should resolve the identifier and replay it', async () => { + const { projections, replay } = await createDiscoveredProjections(); + + await projections.replayForModel(OpSummary); + + const request = replay.mock.calls[0][0]; + expect(request.ObserverId).toEqual('OpSummary'); + }); + }); + + describe('when querying a projection declaration', () => { + it('should call Preview and return the read model entries', async () => { + const { projections, preview } = await createDiscoveredProjections(); + + const result = await projections.query('projection Orders\n from OpStateChanged'); + + expect(preview).toHaveBeenCalledTimes(1); + const request = preview.mock.calls[0][0]; + expect(request.EventSequenceId).toEqual('event-log'); + expect(result.readModelEntries).toEqual(['{"id":"1"}']); + }); + }); + + describe('when querying a projection declaration with a custom event sequence', () => { + it('should pass the given event sequence identifier', async () => { + const { projections, preview } = await createDiscoveredProjections(); + + await projections.query('projection Orders\n from OpStateChanged', 'inbox'); + + const request = preview.mock.calls[0][0]; + expect(request.EventSequenceId).toEqual('inbox'); + }); + }); + + describe('when querying a projection declaration that fails to parse', () => { + it('should throw UnableToQueryProjection with the syntax errors', async () => { + const { projections, preview } = await createDiscoveredProjections(); + preview.mockResolvedValueOnce({ + Value1: { Errors: [{ Message: 'Unexpected token', Line: 2, Column: 3 }] } + }); + + await expect(projections.query('projection Bad')).rejects.toThrow(UnableToQueryProjection); + }); + }); +}); diff --git a/Source/projections/Projections.spec.ts b/Source/projections/Projections.spec.ts index f39920b..f35960b 100644 --- a/Source/projections/Projections.spec.ts +++ b/Source/projections/Projections.spec.ts @@ -85,7 +85,7 @@ function createProjections(readModels: (new (...args: unknown[]) => unknown)[]) eventTypeMigrations: [] }; - const projections = new Projections('test-store', connection, clientArtifacts, 'test-sink'); + const projections = new Projections('test-store', 'test-namespace', connection, clientArtifacts, 'test-sink'); return { projections, registerMock }; } diff --git a/Source/projections/Projections.ts b/Source/projections/Projections.ts index eb33cfa..5e4e072 100644 --- a/Source/projections/Projections.ts +++ b/Source/projections/Projections.ts @@ -12,6 +12,11 @@ import { ChronicleConnection } from '../connection'; import { toContractsGuid } from '../connection/Guid'; import { WellKnownSinks } from '../sinks'; import { EventSequenceId } from '../eventSequences/EventSequenceId'; +import { EventSequenceNumber } from '../eventSequences/EventSequenceNumber'; +import { JobId } from '../jobs/JobId'; +import { FailedPartition } from '../observation/FailedPartition'; +import { FailedPartitions } from '../observation/FailedPartitions'; +import { toObserverRunningState } from '../observation/toObserverRunningState'; import { getReadModelMetadata } from '../readModels'; import { TypeIntrospector } from '../types'; import { IProjections } from './IProjections'; @@ -29,15 +34,19 @@ import { toContractEventType } from './modelBound/childrenAndNestedBuilder'; import { getChildrenFromMetadata } from './modelBound/childrenFrom'; +import { getEventSequenceMetadata } from './modelBound/eventSequence'; import { getFromEveryMetadata } from './modelBound/fromEvery'; import { getFromEventMetadata, hasFromEventMetadata } from './modelBound/fromEvent'; import { getJoinMetadata } from './modelBound/join'; import { ProjectionId } from './ProjectionId'; +import { ProjectionQueryResult } from './ProjectionQueryResult'; +import { ProjectionState } from './ProjectionState'; import { isNested } from './modelBound/nested'; import { isNotRewindable } from './modelBound/notRewindable'; import { isPassive } from './modelBound/passive'; import { getRemovedWithClassMetadata, getRemovedWithPropertyMetadata } from './modelBound/removedWith'; import { getRemovedWithJoinClassMetadata, getRemovedWithJoinPropertyMetadata } from './modelBound/removedWithJoin'; +import { UnableToQueryProjection } from './UnableToQueryProjection'; interface ResolvedModelBoundMetadata { id: ProjectionId; @@ -52,19 +61,27 @@ interface ResolvedModelBoundMetadata { export class Projections implements IProjections { private readonly _declarative = new Map(); private readonly _modelBound = new Map(); + private readonly _failedPartitions: FailedPartitions; private readonly _logger = diag.createComponentLogger({ namespace: '@cratis/chronicle/projections' }); /** * Creates a new {@link Projections} instance. + * @param _eventStore - The event store name. + * @param _namespace - The event store namespace. + * @param _connection - Chronicle connection. * @param _clientArtifacts - Provider for discovered client artifact types. + * @param _defaultSinkTypeId - The identifier of the default read model sink. */ constructor( private readonly _eventStore: string, + private readonly _namespace: string, private readonly _connection: ChronicleConnection, private readonly _clientArtifacts: IClientArtifactsProvider, private readonly _defaultSinkTypeId: string - ) {} + ) { + this._failedPartitions = new FailedPartitions(_eventStore, _namespace, _connection); + } /** @inheritdoc */ async discover(): Promise { @@ -137,6 +154,148 @@ export class Projections implements IProjections { } } + /** @inheritdoc */ + hasFor(projectionId: ProjectionId | string): boolean { + const id = this.toProjectionIdValue(projectionId); + return this._declarative.has(id) || this._modelBound.has(id); + } + + /** @inheritdoc */ + hasForModel(readModelType: Constructor): boolean { + try { + this.resolveProjectionIdForModel(readModelType); + return true; + } catch { + return false; + } + } + + /** @inheritdoc */ + getProjectionIdFor(readModelType: Constructor): ProjectionId { + return this.resolveProjectionIdForModel(readModelType); + } + + /** @inheritdoc */ + async getStateFor(projectionId: ProjectionId | string): Promise { + const id = this.toProjectionIdValue(projectionId); + const eventSequenceId = this.resolveEventSequenceIdFor(id); + + const response = await this._connection.observers.getObserverInformation({ + EventStore: this._eventStore, + Namespace: this._namespace, + ObserverId: id, + EventSequenceId: eventSequenceId + }); + + return { + runningState: toObserverRunningState(response.RunningState), + isSubscribed: response.IsSubscribed, + nextEventSequenceNumber: new EventSequenceNumber(response.NextEventSequenceNumber), + lastHandledEventSequenceNumber: new EventSequenceNumber(response.LastHandledEventSequenceNumber), + tailEventSequenceNumber: new EventSequenceNumber(response.TailEventSequenceNumber) + }; + } + + /** @inheritdoc */ + getStateForModel(readModelType: Constructor): Promise { + return this.getStateFor(this.resolveProjectionIdForModel(readModelType)); + } + + /** @inheritdoc */ + getFailedPartitionsForModel(readModelType: Constructor): Promise { + const projectionId = this.resolveProjectionIdForModel(readModelType); + return this._failedPartitions.getFailedPartitionsFor(projectionId.value); + } + + /** @inheritdoc */ + async replay(projectionId: ProjectionId | string): Promise { + const id = this.toProjectionIdValue(projectionId); + + const response = await this._connection.observers.replay({ + EventStore: this._eventStore, + Namespace: this._namespace, + ObserverId: id, + + // The kernel resolves the observer's own event sequence from its identifier - it does not + // need to be told which one to replay. + EventSequenceId: '' + }); + + return response.JobId ? JobId.from(response.JobId) : JobId.from(Guid.empty); + } + + /** @inheritdoc */ + replayForModel(readModelType: Constructor): Promise { + return this.replay(this.resolveProjectionIdForModel(readModelType)); + } + + /** @inheritdoc */ + async query(declaration: string, eventSequenceId: string = EventSequenceId.eventLog.value): Promise { + const result = await this._connection.projections.preview({ + EventStore: this._eventStore, + Namespace: this._namespace, + EventSequenceId: eventSequenceId, + Declaration: declaration + }); + + if (result.Value1) { + throw new UnableToQueryProjection(result.Value1.Errors.map(error => error.Message)); + } + + return { + readModelEntries: result.Value0?.ReadModelEntries ?? [] + }; + } + + private toProjectionIdValue(projectionId: ProjectionId | string): string { + return typeof projectionId === 'string' ? projectionId : projectionId.value; + } + + /** + * Resolves the {@link ProjectionId} of the projection that maintains a specific read model type, + * from what has already been discovered. + * @param readModelType - The read model type to resolve for. + * @returns The resolved {@link ProjectionId}. + */ + private resolveProjectionIdForModel(readModelType: Constructor): ProjectionId { + for (const [id, type] of this._modelBound) { + if (type === readModelType) { + return new ProjectionId(id); + } + } + + for (const [id, type] of this._declarative) { + const metadata = getProjectionMetadata(type); + if (metadata?.readModelType === readModelType) { + return new ProjectionId(id); + } + } + + throw new Error( + `No projection found for read model '${readModelType.name}'. Make sure discover() has run and the ` + + 'read model is model-bound, or its declarative projection() decorator specifies an explicit readModelType.'); + } + + /** + * Resolves the event sequence identifier a discovered projection reads from, without requiring + * its full definition to have been built. + * @param projectionId - The projection identifier to resolve for. + * @returns The resolved event sequence identifier. + */ + private resolveEventSequenceIdFor(projectionId: string): string { + const modelBoundType = this._modelBound.get(projectionId); + if (modelBoundType) { + return getEventSequenceMetadata(modelBoundType) ?? EventSequenceId.eventLog.value; + } + + const declarativeType = this._declarative.get(projectionId); + if (declarativeType) { + return getProjectionMetadata(declarativeType)?.eventSequenceId ?? EventSequenceId.eventLog.value; + } + + return EventSequenceId.eventLog.value; + } + private async registerWithRetry(projection: unknown, identifier: string, maxAttempts = 5): Promise { let delay = 2000; for (let attempt = 1; attempt <= maxAttempts; attempt++) { diff --git a/Source/projections/UnableToQueryProjection.ts b/Source/projections/UnableToQueryProjection.ts new file mode 100644 index 0000000..79a60fa --- /dev/null +++ b/Source/projections/UnableToQueryProjection.ts @@ -0,0 +1,17 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/** + * Error thrown when a projection query fails because the Projection Declaration Language + * declaration contains errors. + */ +export class UnableToQueryProjection extends Error { + /** + * Creates a new {@link UnableToQueryProjection}. + * @param errors - The collection of error messages describing why the query failed. + */ + constructor(errors: ReadonlyArray) { + super(`Unable to query projection. Errors:\n${errors.join('\n')}`); + this.name = 'UnableToQueryProjection'; + } +} diff --git a/Source/projections/index.ts b/Source/projections/index.ts index 22355fc..4e6e73b 100644 --- a/Source/projections/index.ts +++ b/Source/projections/index.ts @@ -4,5 +4,8 @@ export { ProjectionId } from './ProjectionId'; export type { IProjections } from './IProjections'; export { Projections } from './Projections'; +export type { ProjectionState } from './ProjectionState'; +export type { ProjectionQueryResult } from './ProjectionQueryResult'; +export { UnableToQueryProjection } from './UnableToQueryProjection'; export * from './declarative'; export * from './modelBound'; diff --git a/Source/projections/modelBound/eventSequence.ts b/Source/projections/modelBound/eventSequence.ts new file mode 100644 index 0000000..d66f5ed --- /dev/null +++ b/Source/projections/modelBound/eventSequence.ts @@ -0,0 +1,38 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { EventSequenceId } from '../../eventSequences/EventSequenceId'; + +const METADATA_KEY = 'chronicle:projection:eventSequence'; + +/** + * Class decorator that overrides the event sequence a model-bound projection reads from. + * When applied, auto-inbox routing is suppressed - the explicit value is always honored. + * @param sequence - The event sequence identifier to read from. + * @returns A class decorator. + */ +export function eventSequence(sequence: string): ClassDecorator { + return (target: object) => { + Reflect.defineMetadata(METADATA_KEY, sequence, target); + }; +} + +/** + * Convenience class decorator that pins a model-bound projection to the default event log + * sequence. Equivalent to `eventSequence(EventSequenceId.eventLog.value)`, but more expressive + * about the intent to read from the local event log rather than an inbox or another sequence. + * @param target - The class constructor. + */ +export function eventLog(target: Function): void { + Reflect.defineMetadata(METADATA_KEY, EventSequenceId.eventLog.value, target); +} + +/** + * Retrieves the explicit event sequence identifier stored on a class, if any. + * @param target - The class constructor. + * @returns The event sequence identifier, or undefined when the class has no explicit override. + */ +export function getEventSequenceMetadata(target: Function): string | undefined { + return Reflect.getMetadata(METADATA_KEY, target); +} diff --git a/Source/projections/modelBound/index.ts b/Source/projections/modelBound/index.ts index 30b9270..7f1c392 100644 --- a/Source/projections/modelBound/index.ts +++ b/Source/projections/modelBound/index.ts @@ -35,3 +35,4 @@ export { setValue, getSetValueMetadata } from './setValue'; export type { SetValueMetadata } from './setValue'; export { fromEvery, getFromEveryMetadata } from './fromEvery'; export type { FromEveryMetadata } from './fromEvery'; +export { eventSequence, eventLog, getEventSequenceMetadata } from './eventSequence'; From f3c2a96522c90190ee54c99509b8174d3a23af73 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 13:41:52 +0200 Subject: [PATCH 36/39] Add model-bound completeness: fromAll, noAutoMap, event sequence, clearWith Closes four model-bound gaps against .NET's model-bound attributes: - @fromAll - .NET's FromAllAttribute; TypeScript only had @fromEvery. Model-bound feeds both into the same All-properties map, so fromAll is wired the same way. - @noAutoMap - class and property level, matching NoAutoMapAttribute. buildModelBoundDefinition() hardcoded AutoMap.Enabled and never populated NoAutoMapProperties, so AutoMap could not be disabled and a colliding event property could silently overwrite an explicitly sourced value. - @eventSequence/@eventLog - resolveModelBoundMetadata() always returned eventSequenceId: undefined, pinning every model-bound projection to the event log regardless of what the model declared. - Root-level @clearWith on a scalar property - previously only consumed inside childrenAndNestedBuilder.ts for child/nested types. A root scalar clearWith now adds a $null mapping to that property's From entry, clearing it back to no value whenever the event occurs. --- ...Projections.modelBoundCompleteness.spec.ts | 224 ++++++++++++++++++ Source/projections/Projections.ts | 24 +- Source/projections/modelBound/fromAll.ts | 42 ++++ Source/projections/modelBound/index.ts | 3 + Source/projections/modelBound/noAutoMap.ts | 47 ++++ 5 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 Source/projections/Projections.modelBoundCompleteness.spec.ts create mode 100644 Source/projections/modelBound/fromAll.ts create mode 100644 Source/projections/modelBound/noAutoMap.ts diff --git a/Source/projections/Projections.modelBoundCompleteness.spec.ts b/Source/projections/Projections.modelBoundCompleteness.spec.ts new file mode 100644 index 0000000..836fe6a --- /dev/null +++ b/Source/projections/Projections.modelBoundCompleteness.spec.ts @@ -0,0 +1,224 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { AutoMap } from '@cratis/chronicle.contracts'; +import { describe, expect, it, vi } from 'vitest'; +import { IClientArtifactsProvider } from '../artifacts'; +import { ChronicleConnection } from '../connection'; +import { eventType } from '../events/eventTypeDecorator'; +import { readModel } from '../readModels/readModel'; +import { clearWith } from './modelBound/clearWith'; +import { eventLog, eventSequence } from './modelBound/eventSequence'; +import { fromAll } from './modelBound/fromAll'; +import { fromEvent } from './modelBound/fromEvent'; +import { noAutoMap } from './modelBound/noAutoMap'; +import { setFrom } from './modelBound/setFrom'; +import { Projections } from './Projections'; + +// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these +// fixtures don't depend on the test runner's decorator-syntax support. + +class MbWorkArrangementSet { + location!: string; + workMode!: number; +} +eventType()(MbWorkArrangementSet); + +class MbCandidateSubmitted { + name!: string; + location!: string; +} +eventType()(MbCandidateSubmitted); + +class MbAssignmentSummary { + id!: string; + location!: string; + candidateName!: string; +} +setFrom(MbWorkArrangementSet, 'location')(MbAssignmentSummary.prototype, 'location'); +noAutoMap(MbAssignmentSummary.prototype, 'location'); +setFrom(MbCandidateSubmitted, 'name')(MbAssignmentSummary.prototype, 'candidateName'); +fromEvent(MbWorkArrangementSet)(MbAssignmentSummary); +readModel()(MbAssignmentSummary); + +class MbFullyExcluded { + id!: string; + location!: string; +} +setFrom(MbWorkArrangementSet, 'location')(MbFullyExcluded.prototype, 'location'); +fromEvent(MbWorkArrangementSet)(MbFullyExcluded); +noAutoMap(MbFullyExcluded); +readModel()(MbFullyExcluded); + +class MbProductRenamed { + name!: string; + version!: number; +} +eventType()(MbProductRenamed); + +class MbProductPriceChanged { + price!: number; + version!: number; +} +eventType()(MbProductPriceChanged); + +class MbProductVersion { + id!: string; + name!: string; + price!: number; + version!: number; +} +fromAll('version')(MbProductVersion.prototype, 'version'); +fromEvent(MbProductRenamed)(MbProductVersion); +fromEvent(MbProductPriceChanged)(MbProductVersion); +readModel()(MbProductVersion); + +class MbOrderPlaced { + amount!: number; +} +eventType()(MbOrderPlaced); + +class MbOrderSummaryWithCustomSequence { + id!: string; + totalAmount!: number; +} +setFrom(MbOrderPlaced, 'amount')(MbOrderSummaryWithCustomSequence.prototype, 'totalAmount'); +fromEvent(MbOrderPlaced)(MbOrderSummaryWithCustomSequence); +eventSequence('custom-sequence')(MbOrderSummaryWithCustomSequence); +readModel()(MbOrderSummaryWithCustomSequence); + +class MbLocalEvent { + data!: string; +} +eventType()(MbLocalEvent); + +class MbLocalSnapshot { + id!: string; + data!: string; +} +setFrom(MbLocalEvent, 'data')(MbLocalSnapshot.prototype, 'data'); +fromEvent(MbLocalEvent)(MbLocalSnapshot); +eventLog(MbLocalSnapshot); +readModel()(MbLocalSnapshot); + +class MbProjectNoted { + note!: string; +} +eventType()(MbProjectNoted); + +class MbProjectNoteCleared {} +eventType()(MbProjectNoteCleared); + +class MbProjectNotes { + id!: string; + note!: string | undefined; +} +setFrom(MbProjectNoted, 'note')(MbProjectNotes.prototype, 'note'); +clearWith(MbProjectNoteCleared)(MbProjectNotes.prototype, 'note'); +fromEvent(MbProjectNoted)(MbProjectNotes); +readModel()(MbProjectNotes); + +interface BuiltFromEntry { + Key: { Id: string }; + Value: { Properties: Record; Key: string; ParentKey: string }; +} + +interface BuiltDefinition { + EventSequenceId: string; + AutoMap: AutoMap; + NoAutoMapProperties: string[]; + All: { Properties: Record }; + From: BuiltFromEntry[]; +} + +function createProjections(readModels: (new (...args: unknown[]) => unknown)[]) { + const registerMock = vi.fn().mockResolvedValue(undefined); + const registerManyMock = vi.fn().mockResolvedValue(undefined); + const connection = { + readModels: { registerMany: registerManyMock }, + projections: { register: registerMock } + } as unknown as ChronicleConnection; + + const clientArtifacts: IClientArtifactsProvider = { + eventTypes: [], + readModels: readModels as unknown as IClientArtifactsProvider['readModels'], + reactors: [], + reducers: [], + seeders: [], + constraints: [], + projections: [], + webhooks: [], + eventTypeMigrations: [] + }; + + const projections = new Projections('test-store', 'test-namespace', connection, clientArtifacts, 'test-sink'); + return { projections, registerMock }; +} + +async function registerAndGetDefinition(readModelType: new (...args: unknown[]) => unknown): Promise { + const { projections, registerMock } = createProjections([readModelType]); + await projections.register(); + return registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition; +} + +function findFromEntry(definition: BuiltDefinition, eventTypeId: string): BuiltFromEntry { + const entry = definition.From.find(candidate => candidate.Key.Id === eventTypeId); + if (!entry) { + throw new Error(`No From entry found for event type '${eventTypeId}'.`); + } + return entry; +} + +describe('Projections model-bound completeness', () => { + describe('when a property is excluded from AutoMap with noAutoMap', () => { + it('should keep AutoMap enabled at the root and list only the excluded property', async () => { + const definition = await registerAndGetDefinition(MbAssignmentSummary); + + expect(definition.AutoMap).toBe(AutoMap.Enabled); + expect(definition.NoAutoMapProperties).toEqual(['location']); + + const workArrangementSet = findFromEntry(definition, 'MbWorkArrangementSet'); + expect(workArrangementSet.Value.Properties.location).toBe('location'); + + const candidateSubmitted = findFromEntry(definition, 'MbCandidateSubmitted'); + expect(candidateSubmitted.Value.Properties.candidateName).toBe('name'); + }); + }); + + describe('when a whole model-bound projection is excluded from AutoMap with class-level noAutoMap', () => { + it('should disable AutoMap for the whole definition', async () => { + const definition = await registerAndGetDefinition(MbFullyExcluded); + expect(definition.AutoMap).toBe(AutoMap.Disabled); + }); + }); + + describe('when a property uses fromAll to read from every event type', () => { + it('should include it in the All properties map', async () => { + const definition = await registerAndGetDefinition(MbProductVersion); + expect(definition.All.Properties.version).toBe('version'); + }); + }); + + describe('when a model-bound projection declares a custom event sequence', () => { + it('should use the declared event sequence instead of the event log', async () => { + const definition = await registerAndGetDefinition(MbOrderSummaryWithCustomSequence); + expect(definition.EventSequenceId).toBe('custom-sequence'); + }); + }); + + describe('when a model-bound projection is explicitly pinned to the event log', () => { + it('should use the event log sequence', async () => { + const definition = await registerAndGetDefinition(MbLocalSnapshot); + expect(definition.EventSequenceId).toBe('event-log'); + }); + }); + + describe('when a root scalar property is cleared with an event', () => { + it('should add a $null mapping for that property on the clearing event', async () => { + const definition = await registerAndGetDefinition(MbProjectNotes); + const clearedBy = findFromEntry(definition, 'MbProjectNoteCleared'); + expect(clearedBy.Value.Properties.note).toBe('$null'); + }); + }); +}); diff --git a/Source/projections/Projections.ts b/Source/projections/Projections.ts index 5e4e072..781c9ef 100644 --- a/Source/projections/Projections.ts +++ b/Source/projections/Projections.ts @@ -29,15 +29,19 @@ import { buildNestedEntry, ChildrenDefinitionLike, ContractEventType, + ensureFromEntry, FromRecord, getEventTypeMapKey, toContractEventType } from './modelBound/childrenAndNestedBuilder'; import { getChildrenFromMetadata } from './modelBound/childrenFrom'; +import { getClearWithPropertyMetadata } from './modelBound/clearWith'; import { getEventSequenceMetadata } from './modelBound/eventSequence'; +import { getFromAllMetadata } from './modelBound/fromAll'; import { getFromEveryMetadata } from './modelBound/fromEvery'; import { getFromEventMetadata, hasFromEventMetadata } from './modelBound/fromEvent'; import { getJoinMetadata } from './modelBound/join'; +import { isNoAutoMap, isPropertyNoAutoMap } from './modelBound/noAutoMap'; import { ProjectionId } from './ProjectionId'; import { ProjectionQueryResult } from './ProjectionQueryResult'; import { ProjectionState } from './ProjectionState'; @@ -503,14 +507,25 @@ export class Projections implements IProjections { childrenByProperty[property] = buildChildrenEntry(type, property, childrenFromList); } - if (isNested(prototype, property)) { + const propertyIsNested = isNested(prototype, property); + if (propertyIsNested) { nestedByProperty[property] = buildNestedEntry(type, property); } + + // A scalar (non-nested, non-children-collection) root property clears back to no value + // every time the given event is observed. A nested single-object property's clearWith is + // handled by buildNestedEntry instead, which clears the whole nested object. + if (childrenFromList.length === 0 && !propertyIsNested) { + for (const clearWith of getClearWithPropertyMetadata(prototype, property)) { + const entry = ensureFromEntry(fromByEventType, clearWith.eventType); + entry.Value.Properties[property] = '$null'; + } + } } const allProperties: Record = {}; for (const property of properties) { - const fromEvery = getFromEveryMetadata(prototype, property); + const fromEvery = getFromEveryMetadata(prototype, property) ?? getFromAllMetadata(prototype, property); if (fromEvery) { allProperties[property] = fromEvery.contextProperty ? fromEvery.contextProperty @@ -538,7 +553,8 @@ export class Projections implements IProjections { RemovedWithJoin: Array.from(removedWithJoinByEventType.values()), LastUpdated: { Value: '' }, Tags: [], - AutoMap: AutoMap.Enabled, + AutoMap: isNoAutoMap(type) ? AutoMap.Disabled : AutoMap.Enabled, + NoAutoMapProperties: properties.filter(property => isPropertyNoAutoMap(prototype, property)), Nested: nestedByProperty }; definition.LastUpdated = { Value: this.computeStableLastUpdated(definition) }; @@ -551,7 +567,7 @@ export class Projections implements IProjections { if (readModelMetadata && hasFromEventMetadata(type)) { return { id: new ProjectionId(readModelMetadata.id.value), - eventSequenceId: undefined, + eventSequenceId: getEventSequenceMetadata(type), readModelIdentifier: readModelMetadata.id.value }; } diff --git a/Source/projections/modelBound/fromAll.ts b/Source/projections/modelBound/fromAll.ts new file mode 100644 index 0000000..fb5e129 --- /dev/null +++ b/Source/projections/modelBound/fromAll.ts @@ -0,0 +1,42 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { TypeIntrospector } from '../../types'; + +/** Metadata stored by the fromAll property decorator. */ +export interface FromAllMetadata { + /** The event property name to read the value from. */ + readonly property?: string; + /** The event context property name to read the value from. */ + readonly contextProperty?: string; +} + +const METADATA_KEY = 'chronicle:projection:fromAll'; + +/** + * Property decorator that sets the decorated read model property from a property present on every + * event type the projection is built from - the convention-based counterpart to declaring the same + * mapping on every individual `fromEvent`. Equivalent to {@link fromEvery} for model-bound + * projections; use whichever name reads better for the property being mapped. + * @param property - Optional event property name. If not specified, uses the model property name. + * @param contextProperty - Optional event context property name. + * @returns A property decorator. + */ +export function fromAll(property?: string, contextProperty?: string): PropertyDecorator { + return (target: object, propertyKey: string | symbol) => { + TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, propertyKey.toString()); + const metadata: FromAllMetadata = { property, contextProperty }; + Reflect.defineMetadata(METADATA_KEY, metadata, target, propertyKey.toString()); + }; +} + +/** + * Retrieves fromAll metadata stored on the given property. + * @param target - The class prototype. + * @param propertyKey - The property name. + * @returns The fromAll metadata, or undefined if not decorated. + */ +export function getFromAllMetadata(target: object, propertyKey: string): FromAllMetadata | undefined { + return Reflect.getMetadata(METADATA_KEY, target, propertyKey); +} diff --git a/Source/projections/modelBound/index.ts b/Source/projections/modelBound/index.ts index 7f1c392..3a9fa00 100644 --- a/Source/projections/modelBound/index.ts +++ b/Source/projections/modelBound/index.ts @@ -35,4 +35,7 @@ export { setValue, getSetValueMetadata } from './setValue'; export type { SetValueMetadata } from './setValue'; export { fromEvery, getFromEveryMetadata } from './fromEvery'; export type { FromEveryMetadata } from './fromEvery'; +export { fromAll, getFromAllMetadata } from './fromAll'; +export type { FromAllMetadata } from './fromAll'; +export { noAutoMap, isNoAutoMap, isPropertyNoAutoMap } from './noAutoMap'; export { eventSequence, eventLog, getEventSequenceMetadata } from './eventSequence'; diff --git a/Source/projections/modelBound/noAutoMap.ts b/Source/projections/modelBound/noAutoMap.ts new file mode 100644 index 0000000..bfd43ef --- /dev/null +++ b/Source/projections/modelBound/noAutoMap.ts @@ -0,0 +1,47 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { TypeIntrospector } from '../../types'; + +const CLASS_METADATA_KEY = 'chronicle:projection:noAutoMap:class'; +const PROPERTY_METADATA_KEY = 'chronicle:projection:noAutoMap:property'; + +/** + * Class or property decorator that disables AutoMap for a model-bound projection. + * Applied to a class, it prevents AutoMap from mapping any property automatically. + * Applied to a property, it excludes only that single property from AutoMap while every other + * property keeps mapping - use it to stop an unrelated event that carries an identically named + * property from silently overwriting a property whose value is set explicitly (for example via + * `setFrom`). + * @param target - The class constructor, or the class prototype when used on a property. + * @param propertyKey - The property name, when used as a property decorator. + */ +export function noAutoMap(target: object, propertyKey?: string | symbol): void { + if (propertyKey !== undefined) { + const key = propertyKey.toString(); + TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key); + Reflect.defineMetadata(PROPERTY_METADATA_KEY, true, target, key); + } else { + Reflect.defineMetadata(CLASS_METADATA_KEY, true, target as Function); + } +} + +/** + * Checks whether the given class has AutoMap disabled entirely. + * @param target - The class constructor. + * @returns True if the class is marked with {@link noAutoMap}; false otherwise. + */ +export function isNoAutoMap(target: Function): boolean { + return Reflect.hasMetadata(CLASS_METADATA_KEY, target); +} + +/** + * Checks whether a specific property is excluded from AutoMap. + * @param target - The class prototype. + * @param propertyKey - The property name. + * @returns True if the property is marked with {@link noAutoMap}; false otherwise. + */ +export function isPropertyNoAutoMap(target: object, propertyKey: string): boolean { + return Reflect.hasMetadata(PROPERTY_METADATA_KEY, target, propertyKey); +} From dc4c70dc566df677f3d55a5eb611f556912a7f69 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 13:43:43 +0200 Subject: [PATCH 37/39] Un-fence projection client snippets now supported in TypeScript Replace the "does not support this workflow yet" placeholders with real snippets for the model-bound and PDL-query workflows landed in the previous two commits: noAutoMap (class and property), fromAll, eventSequence/eventLog, root-level clearWith, and query(). Register the five adhoc-querying snippets in validate-client-snippets.py's BODY_SNIPPETS so they get the store parameter they call store.projections.query() through, matching how every other store-calling snippet is wired into the generated compilation unit. Every other workflow's fence is left in place - only what actually works now is un-fenced. --- .../projections/model-bound/clearing/basic.md | 20 +++++++++-- .../model-bound/clearing/nullable.md | 29 ++++++++++++++-- .../convention-based/no-auto-map-property.md | 34 +++++++++++++++++-- .../event-sequence-source/event-log.md | 17 ++++++++-- .../event-sequence-source/model-bound.md | 17 ++++++++-- .../from-all/attribute-convention.md | 31 +++++++++++++++-- .../adhoc-querying/basic.md | 13 +++++-- .../adhoc-querying/custom-sequence.md | 9 +++-- .../adhoc-querying/error-handling.md | 15 ++++++-- .../adhoc-querying/inferred-vs-explicit.md | 16 +++++++-- .../adhoc-querying/type-mismatch.md | 10 ++++-- Documentation/validate-client-snippets.py | 5 +++ 12 files changed, 194 insertions(+), 22 deletions(-) diff --git a/Documentation/client-snippets/projections/model-bound/clearing/basic.md b/Documentation/client-snippets/projections/model-bound/clearing/basic.md index 19b3207..2be1d9b 100644 --- a/Documentation/client-snippets/projections/model-bound/clearing/basic.md +++ b/Documentation/client-snippets/projections/model-bound/clearing/basic.md @@ -1,3 +1,19 @@ -```text -TypeScript does not support this workflow yet. +```typescript title="Clear a scalar member" +import { clearWith, eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbClearingProjectNoted { + note = ''; +} + +@eventType() +class MbClearingProjectNoteCleared {} + +@readModel() +@fromEvent(MbClearingProjectNoted) +class MbClearingProjectNotes { + @setFrom(MbClearingProjectNoted, 'note') + @clearWith(MbClearingProjectNoteCleared) + note: string | undefined = undefined; +} ``` diff --git a/Documentation/client-snippets/projections/model-bound/clearing/nullable.md b/Documentation/client-snippets/projections/model-bound/clearing/nullable.md index 19b3207..08af8a2 100644 --- a/Documentation/client-snippets/projections/model-bound/clearing/nullable.md +++ b/Documentation/client-snippets/projections/model-bound/clearing/nullable.md @@ -1,3 +1,28 @@ -```text -TypeScript does not support this workflow yet. +```typescript title="A member has to be able to hold no value" +import { clearWith, eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbClearingShiftPlanned { + constructor( + readonly assignee: string, + readonly hours: number + ) {} +} + +@eventType() +class MbClearingShiftReleased {} + +@readModel() +@fromEvent(MbClearingShiftPlanned) +class MbClearingShift { + // Optional, so "nobody is assigned" is a state the member can actually hold. + @setFrom(MbClearingShiftPlanned, 'assignee') + @clearWith(MbClearingShiftReleased) + assignee: string | undefined = undefined; + + // Optional for the same reason: 0 hours is a number of hours, not the absence of one. + @setFrom(MbClearingShiftPlanned, 'hours') + @clearWith(MbClearingShiftReleased) + hours: number | undefined = undefined; +} ``` diff --git a/Documentation/client-snippets/projections/model-bound/convention-based/no-auto-map-property.md b/Documentation/client-snippets/projections/model-bound/convention-based/no-auto-map-property.md index 19b3207..1f625f7 100644 --- a/Documentation/client-snippets/projections/model-bound/convention-based/no-auto-map-property.md +++ b/Documentation/client-snippets/projections/model-bound/convention-based/no-auto-map-property.md @@ -1,3 +1,33 @@ -```text -TypeScript does not support this workflow yet. +```typescript title="Exclude a single property from convention mapping" +import { eventType, fromEvent, noAutoMap, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class NoAutoMapWorkArrangementSet { + constructor( + readonly location: string, + readonly workMode: number + ) {} +} + +@eventType() +class NoAutoMapCandidateSubmitted { + constructor( + readonly name: string, + readonly location: string + ) {} +} + +@readModel() +@fromEvent(NoAutoMapWorkArrangementSet) +class NoAutoMapAssignmentSummary { + // location is sourced only from NoAutoMapWorkArrangementSet. NoAutoMapCandidateSubmitted is + // value-mapped (for candidateName) and also carries a location; @noAutoMap stops that location + // from being auto-mapped over the explicit value, while every other property keeps mapping. + @setFrom(NoAutoMapWorkArrangementSet, 'location') + @noAutoMap + location = ''; + + @setFrom(NoAutoMapCandidateSubmitted, 'name') + candidateName = ''; +} ``` diff --git a/Documentation/client-snippets/projections/model-bound/event-sequence-source/event-log.md b/Documentation/client-snippets/projections/model-bound/event-sequence-source/event-log.md index 19b3207..46d698a 100644 --- a/Documentation/client-snippets/projections/model-bound/event-sequence-source/event-log.md +++ b/Documentation/client-snippets/projections/model-bound/event-sequence-source/event-log.md @@ -1,3 +1,16 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { eventLog, eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbEventSeqLocalEvent { + data = ''; +} + +@readModel() +@fromEvent(MbEventSeqLocalEvent) +@eventLog +class MbEventSeqLocalSnapshot { + @setFrom(MbEventSeqLocalEvent, 'data') + data = ''; +} ``` diff --git a/Documentation/client-snippets/projections/model-bound/event-sequence-source/model-bound.md b/Documentation/client-snippets/projections/model-bound/event-sequence-source/model-bound.md index 19b3207..a0d8881 100644 --- a/Documentation/client-snippets/projections/model-bound/event-sequence-source/model-bound.md +++ b/Documentation/client-snippets/projections/model-bound/event-sequence-source/model-bound.md @@ -1,3 +1,16 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { eventSequence, eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbEventSeqOrderPlaced { + amount = 0; +} + +@readModel() +@fromEvent(MbEventSeqOrderPlaced) +@eventSequence('custom-sequence') +class MbEventSeqOrderSummary { + @setFrom(MbEventSeqOrderPlaced, 'amount') + totalAmount = 0; +} ``` diff --git a/Documentation/client-snippets/projections/model-bound/from-all/attribute-convention.md b/Documentation/client-snippets/projections/model-bound/from-all/attribute-convention.md index 19b3207..83743a4 100644 --- a/Documentation/client-snippets/projections/model-bound/from-all/attribute-convention.md +++ b/Documentation/client-snippets/projections/model-bound/from-all/attribute-convention.md @@ -1,3 +1,30 @@ -```text -TypeScript does not support this workflow yet. +```typescript title="Convention-based fromAll property" +import { eventType, fromAll, fromEvent, readModel } from '@cratis/chronicle'; + +@eventType() +class ProductRenamedFromAllConvention { + constructor( + readonly name: string, + readonly version: number + ) {} +} + +@eventType() +class ProductPriceChangedFromAllConvention { + constructor( + readonly price: number, + readonly version: number + ) {} +} + +@readModel() +@fromEvent(ProductRenamedFromAllConvention) +@fromEvent(ProductPriceChangedFromAllConvention) +class ProductVersionFromAllConvention { + name = ''; + price = 0; + + @fromAll() + version = 0; +} ``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/basic.md b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/basic.md index 19b3207..8c7dab2 100644 --- a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/basic.md +++ b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/basic.md @@ -1,3 +1,12 @@ -```text -TypeScript does not support this workflow yet. +```typescript +interface PdlOrderSummary { + orderId: string; +} + +const result = await store.projections.query(` + projection OrderSummary + from OrderPlaced +`); + +const summaries = result.readModelEntries.map(json => JSON.parse(json) as PdlOrderSummary); ``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/custom-sequence.md b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/custom-sequence.md index 19b3207..7e98462 100644 --- a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/custom-sequence.md +++ b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/custom-sequence.md @@ -1,3 +1,8 @@ -```text -TypeScript does not support this workflow yet. +```typescript +const result = await store.projections.query( + ` + projection InboxMessages + from MessageReceived + `, + 'inbox'); ``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/error-handling.md b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/error-handling.md index 19b3207..ea55fb6 100644 --- a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/error-handling.md +++ b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/error-handling.md @@ -1,3 +1,14 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { UnableToQueryProjection } from '@cratis/chronicle'; + +try { + const result = await store.projections.query(` + projection Orders + from OrderPlaced + `); +} catch (error) { + if (error instanceof UnableToQueryProjection) { + console.log(error.message); + } +} ``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/inferred-vs-explicit.md b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/inferred-vs-explicit.md index 19b3207..81fe417 100644 --- a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/inferred-vs-explicit.md +++ b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/inferred-vs-explicit.md @@ -1,3 +1,15 @@ -```text -TypeScript does not support this workflow yet. +```typescript +// Inferred - schema derived from OrderPlaced and OrderShipped event properties +const inferred = await store.projections.query(` + projection Orders + from OrderPlaced + from OrderShipped +`); + +// Explicit - schema comes from the registered 'PdlOrderReadModel' type +const explicitResult = await store.projections.query(` + projection Orders => PdlOrderReadModel + from OrderPlaced + from OrderShipped +`); ``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/type-mismatch.md b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/type-mismatch.md index 19b3207..d534299 100644 --- a/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/type-mismatch.md +++ b/Documentation/client-snippets/projections/projection-declaration-language/adhoc-querying/type-mismatch.md @@ -1,3 +1,9 @@ -```text -TypeScript does not support this workflow yet. +```typescript +// This declaration will throw UnableToQueryProjection: +// OrderPlaced.value is a string, but OrderShipped.value is a number +const result = await store.projections.query(` + projection Bad + from OrderPlaced // value: string + from OrderShipped // value: number -> incompatible types +`); ``` diff --git a/Documentation/validate-client-snippets.py b/Documentation/validate-client-snippets.py index ad05c6e..447a2d1 100644 --- a/Documentation/validate-client-snippets.py +++ b/Documentation/validate-client-snippets.py @@ -50,6 +50,11 @@ "read-models/watching-read-models/filtering": """ const threshold = 1000; """, + "projections/projection-declaration-language/adhoc-querying/basic": "", + "projections/projection-declaration-language/adhoc-querying/inferred-vs-explicit": "", + "projections/projection-declaration-language/adhoc-querying/type-mismatch": "", + "projections/projection-declaration-language/adhoc-querying/custom-sequence": "", + "projections/projection-declaration-language/adhoc-querying/error-handling": "", "contributing/clients/typescript-grpc-package/event-stores-definition": "", "contributing/clients/typescript-grpc-package/namespaces-definition": "", "contributing/clients/typescript-grpc-package/request-messages": "", From 5ed9c3d6bc129c4a6a99d90ca361ddbf7737bfaf Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 14:03:57 +0200 Subject: [PATCH 38/39] Add event tags to the TypeScript client Tag concept, @tag/@tags decorators, @filterEventsByTag for reactors and reducers, tags on AppendOptions and EventContext, and tags in seeding. The wire fields were already being sent as empty arrays. Specs are not yet written for this surface. --- .../tagging-reactors/mixed-approach.md | 24 +++++++- .../multiple-tags-multiple-attributes.md | 25 +++++++- .../multiple-tags-single-attribute.md | 23 +++++++- .../concepts/tagging-reactors/single-tag.md | 23 +++++++- .../tagging-reactors/tag-categories.md | 17 +++++- .../tagging/by-communication-channel.md | 10 +++- .../concepts/tagging/by-domain.md | 10 +++- .../concepts/tagging/by-integration-type.md | 10 +++- .../concepts/tagging/by-purpose.md | 12 +++- .../concepts/tagging/by-stakeholder.md | 11 +++- .../concepts/tagging/dynamic-event-tags.md | 16 ++++- .../concepts/tagging/dynamic-tags-patterns.md | 34 ++++++++++- .../tagging/observer-mixed-approach.md | 15 ++++- ...erver-multiple-tags-multiple-attributes.md | 10 +++- ...observer-multiple-tags-single-attribute.md | 13 +++- .../concepts/tagging/observer-single-tag.md | 8 ++- .../concepts/tagging/read-model-tags.md | 9 ++- .../concepts/tagging/static-event-tags.md | 25 +++++++- .../concepts/tagging/tags-in-event-context.md | 28 ++++++++- .../events/appending-with-tags/append-many.md | 28 ++++++++- .../events/appending-with-tags/append.md | 21 ++++++- .../filtering/by-tag/multiple-filters.md | 16 ++++- .../events/filtering/by-tag/reactor.md | 27 ++++++++- .../events/filtering/by-tag/reducer.md | 35 ++++++++++- .../reactors/filtering/by-tag.md | 26 +++++++- .../reactors/filtering/multiple-tags.md | 16 ++++- .../reducers/filtering/by-tag.md | 39 +++++++++++- .../reducers/filtering/multiple-tags.md | 25 +++++++- .../tagging-reducers/mixed-approach.md | 13 +++- .../multiple-tags-multiple-attributes.md | 14 ++++- .../multiple-tags-single-attribute.md | 12 +++- .../reducers/tagging-reducers/single-tag.md | 28 ++++++++- .../tagging-reducers/tag-categories.md | 19 +++++- Source/eventSequences/AppendOptions.ts | 9 +++ .../eventSequences/EventForEventSourceId.ts | 9 +++ Source/eventSequences/EventSequence.ts | 26 ++++++-- Source/events/EventContext.ts | 4 ++ Source/events/Tag.ts | 14 +++++ Source/events/filterEventsByTagDecorator.ts | 51 ++++++++++++++++ Source/events/index.ts | 4 ++ Source/events/mergeTags.ts | 27 +++++++++ Source/events/tagDecorator.ts | 59 +++++++++++++++++++ Source/reactors/Reactors.ts | 10 +++- Source/reducers/Reducers.spec.ts | 37 ++++++++++++ Source/reducers/Reducers.ts | 25 +++++++- Source/seeding/EventSeeding.ts | 4 +- 46 files changed, 840 insertions(+), 81 deletions(-) create mode 100644 Source/events/Tag.ts create mode 100644 Source/events/filterEventsByTagDecorator.ts create mode 100644 Source/events/mergeTags.ts create mode 100644 Source/events/tagDecorator.ts diff --git a/Documentation/client-snippets/concepts/tagging-reactors/mixed-approach.md b/Documentation/client-snippets/concepts/tagging-reactors/mixed-approach.md index 19b3207..a8886c2 100644 --- a/Documentation/client-snippets/concepts/tagging-reactors/mixed-approach.md +++ b/Documentation/client-snippets/concepts/tagging-reactors/mixed-approach.md @@ -1,3 +1,23 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { EventContext, eventType, reactor, tag } from '@cratis/chronicle'; + +@eventType() +class TaggingReactorsOrderShipped { + constructor(readonly phoneNumber: string, readonly trackingNumber: string) {} +} + +interface TaggingReactorsSmsService { + sendShippingNotification(phoneNumber: string, trackingNumber: string): Promise; +} + +@reactor() +@tag('Notifications', 'SMS') +@tag('Customer') +class TaggingReactorsSmsNotificationReactor { + constructor(private readonly smsService: TaggingReactorsSmsService) {} + + async taggingReactorsOrderShipped(event: TaggingReactorsOrderShipped, _context: EventContext): Promise { + await this.smsService.sendShippingNotification(event.phoneNumber, event.trackingNumber); + } +} ``` diff --git a/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-multiple-attributes.md b/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-multiple-attributes.md index 19b3207..0b7535f 100644 --- a/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-multiple-attributes.md +++ b/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-multiple-attributes.md @@ -1,3 +1,24 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { EventContext, eventType, reactor, tag } from '@cratis/chronicle'; + +@eventType() +class TaggingReactorsProductStockChanged { + constructor(readonly productId: string, readonly newQuantity: number) {} +} + +interface TaggingReactorsInventoryApi { + updateStock(productId: string, newQuantity: number): Promise; +} + +@reactor() +@tag('Integration') +@tag('ExternalAPI') +@tag('Inventory') +class TaggingReactorsInventorySyncReactor { + constructor(private readonly inventoryApi: TaggingReactorsInventoryApi) {} + + async taggingReactorsProductStockChanged(event: TaggingReactorsProductStockChanged, _context: EventContext): Promise { + await this.inventoryApi.updateStock(event.productId, event.newQuantity); + } +} ``` diff --git a/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-single-attribute.md b/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-single-attribute.md index 19b3207..294f053 100644 --- a/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-single-attribute.md +++ b/Documentation/client-snippets/concepts/tagging-reactors/multiple-tags-single-attribute.md @@ -1,3 +1,22 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { EventContext, eventType, reactor, tag } from '@cratis/chronicle'; + +@eventType() +class TaggingReactorsCustomerRegistered { + constructor(readonly email: string, readonly name: string) {} +} + +interface TaggingReactorsWelcomeEmailService { + sendWelcomeEmail(email: string, name: string): Promise; +} + +@reactor() +@tag('Notifications', 'Customer', 'Email') +class TaggingReactorsCustomerNotificationReactor { + constructor(private readonly emailService: TaggingReactorsWelcomeEmailService) {} + + async taggingReactorsCustomerRegistered(event: TaggingReactorsCustomerRegistered, _context: EventContext): Promise { + await this.emailService.sendWelcomeEmail(event.email, event.name); + } +} ``` diff --git a/Documentation/client-snippets/concepts/tagging-reactors/single-tag.md b/Documentation/client-snippets/concepts/tagging-reactors/single-tag.md index 19b3207..2fec910 100644 --- a/Documentation/client-snippets/concepts/tagging-reactors/single-tag.md +++ b/Documentation/client-snippets/concepts/tagging-reactors/single-tag.md @@ -1,3 +1,22 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { EventContext, eventType, reactor, tag } from '@cratis/chronicle'; + +@eventType() +class TaggingReactorsOrderPlaced { + constructor(readonly customerId: string, readonly orderId: string) {} +} + +interface TaggingReactorsEmailService { + sendOrderConfirmation(customerId: string, orderId: string): Promise; +} + +@reactor() +@tag('Notifications') +class TaggingReactorsOrderConfirmationReactor { + constructor(private readonly emailService: TaggingReactorsEmailService) {} + + async taggingReactorsOrderPlaced(event: TaggingReactorsOrderPlaced, _context: EventContext): Promise { + await this.emailService.sendOrderConfirmation(event.customerId, event.orderId); + } +} ``` diff --git a/Documentation/client-snippets/concepts/tagging-reactors/tag-categories.md b/Documentation/client-snippets/concepts/tagging-reactors/tag-categories.md index 19b3207..076797c 100644 --- a/Documentation/client-snippets/concepts/tagging-reactors/tag-categories.md +++ b/Documentation/client-snippets/concepts/tagging-reactors/tag-categories.md @@ -1,3 +1,16 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { reactor, tag } from '@cratis/chronicle'; + +@reactor() +// By integration type +@tag('Notifications', 'ExternalAPI', 'MessageQueue', 'FileSystem') +// By domain +@tag('Sales', 'Inventory', 'Customer', 'Shipping') +// By communication channel +@tag('Email', 'SMS', 'Push', 'Webhook') +// By purpose +@tag('Integration', 'Alerting', 'Monitoring', 'Automation') +// By stakeholder +@tag('Customer', 'Operations', 'Finance', 'Support') +class TaggingReactorsCategoryExamplesReactor {} ``` diff --git a/Documentation/client-snippets/concepts/tagging/by-communication-channel.md b/Documentation/client-snippets/concepts/tagging/by-communication-channel.md index 19b3207..abae036 100644 --- a/Documentation/client-snippets/concepts/tagging/by-communication-channel.md +++ b/Documentation/client-snippets/concepts/tagging/by-communication-channel.md @@ -1,3 +1,9 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { tag } from '@cratis/chronicle'; + +@tag('Email') +@tag('SMS') +@tag('Push') +@tag('Webhook') +class TaggingByCommunicationChannelExample {} ``` diff --git a/Documentation/client-snippets/concepts/tagging/by-domain.md b/Documentation/client-snippets/concepts/tagging/by-domain.md index 19b3207..4855b58 100644 --- a/Documentation/client-snippets/concepts/tagging/by-domain.md +++ b/Documentation/client-snippets/concepts/tagging/by-domain.md @@ -1,3 +1,9 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { tag } from '@cratis/chronicle'; + +@tag('Sales') +@tag('Inventory') +@tag('Customer') +@tag('Shipping') +class TaggingByDomainExample {} ``` diff --git a/Documentation/client-snippets/concepts/tagging/by-integration-type.md b/Documentation/client-snippets/concepts/tagging/by-integration-type.md index 19b3207..cde51f7 100644 --- a/Documentation/client-snippets/concepts/tagging/by-integration-type.md +++ b/Documentation/client-snippets/concepts/tagging/by-integration-type.md @@ -1,3 +1,9 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { tag } from '@cratis/chronicle'; + +@tag('Notifications') +@tag('ExternalAPI') +@tag('MessageQueue') +@tag('FileSystem') +class TaggingByIntegrationTypeExample {} ``` diff --git a/Documentation/client-snippets/concepts/tagging/by-purpose.md b/Documentation/client-snippets/concepts/tagging/by-purpose.md index 19b3207..c8b1738 100644 --- a/Documentation/client-snippets/concepts/tagging/by-purpose.md +++ b/Documentation/client-snippets/concepts/tagging/by-purpose.md @@ -1,3 +1,11 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { tag } from '@cratis/chronicle'; + +@tag('Analytics') +@tag('Reporting') +@tag('Integration') +@tag('Alerting') +@tag('Monitoring') +@tag('Automation') +class TaggingByPurposeExample {} ``` diff --git a/Documentation/client-snippets/concepts/tagging/by-stakeholder.md b/Documentation/client-snippets/concepts/tagging/by-stakeholder.md index 19b3207..1eb3d97 100644 --- a/Documentation/client-snippets/concepts/tagging/by-stakeholder.md +++ b/Documentation/client-snippets/concepts/tagging/by-stakeholder.md @@ -1,3 +1,10 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { tag } from '@cratis/chronicle'; + +@tag('Customer') +@tag('Operations') +@tag('Finance') +@tag('Support') +@tag('Executive') +class TaggingByStakeholderExample {} ``` diff --git a/Documentation/client-snippets/concepts/tagging/dynamic-event-tags.md b/Documentation/client-snippets/concepts/tagging/dynamic-event-tags.md index 19b3207..8bbdd18 100644 --- a/Documentation/client-snippets/concepts/tagging/dynamic-event-tags.md +++ b/Documentation/client-snippets/concepts/tagging/dynamic-event-tags.md @@ -1,3 +1,15 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class TaggingUserLoginService { + constructor(private readonly store: IEventStore) {} + + // The event will end up with four tags: ['analytics', 'user-action', 'production', 'critical'] + async recordLogin(eventSourceId: string): Promise { + await this.store.eventLog.append( + eventSourceId, + new TaggingUserLoggedIn('user123', new Date()), + { tags: ['production', 'critical'] }); + } +} ``` diff --git a/Documentation/client-snippets/concepts/tagging/dynamic-tags-patterns.md b/Documentation/client-snippets/concepts/tagging/dynamic-tags-patterns.md index 19b3207..2511a5b 100644 --- a/Documentation/client-snippets/concepts/tagging/dynamic-tags-patterns.md +++ b/Documentation/client-snippets/concepts/tagging/dynamic-tags-patterns.md @@ -1,3 +1,33 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class TaggingDynamicTagsEventOccurred { + constructor(readonly data: string) {} +} + +class TaggingDynamicTagsService { + constructor(private readonly store: IEventStore) {} + + async recordProductionCritical(eventSourceId: string): Promise { + await this.store.eventLog.append( + eventSourceId, + new TaggingDynamicTagsEventOccurred('production issue'), + { tags: ['production', 'critical'] }); + } + + async recordDevelopmentTest(eventSourceId: string): Promise { + await this.store.eventLog.append( + eventSourceId, + new TaggingDynamicTagsEventOccurred('test run'), + { tags: ['development', 'testing'] }); + } + + async recordBatchMigration(eventSourceId: string): Promise { + await this.store.eventLog.append( + eventSourceId, + new TaggingDynamicTagsEventOccurred('batch migration'), + { tags: ['migration', 'batch-process'] }); + } +} ``` diff --git a/Documentation/client-snippets/concepts/tagging/observer-mixed-approach.md b/Documentation/client-snippets/concepts/tagging/observer-mixed-approach.md index 19b3207..5a510a9 100644 --- a/Documentation/client-snippets/concepts/tagging/observer-mixed-approach.md +++ b/Documentation/client-snippets/concepts/tagging/observer-mixed-approach.md @@ -1,3 +1,14 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { reactor, tag, tags } from '@cratis/chronicle'; + +@reactor() +@tag('Notifications', 'SMS') +@tags('Customer') +class TaggingSmsNotificationReactor {} + +// Or mix single and multiple attributes the other way around +@reactor() +@tag('Integration') +@tags('ExternalAPI', 'Inventory') +class TaggingInventorySyncReactorMixed {} ``` diff --git a/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-multiple-attributes.md b/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-multiple-attributes.md index 19b3207..6e1fcf9 100644 --- a/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-multiple-attributes.md +++ b/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-multiple-attributes.md @@ -1,3 +1,9 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { reactor, tag } from '@cratis/chronicle'; + +@reactor() +@tag('Integration') +@tag('ExternalAPI') +@tag('Inventory') +class TaggingInventorySyncReactor {} ``` diff --git a/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-single-attribute.md b/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-single-attribute.md index 19b3207..9a62cfb 100644 --- a/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-single-attribute.md +++ b/Documentation/client-snippets/concepts/tagging/observer-multiple-tags-single-attribute.md @@ -1,3 +1,12 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { reactor, tag, tags } from '@cratis/chronicle'; + +@reactor() +@tag('Notifications', 'Customer', 'Email') +class TaggingCustomerNotificationReactor {} + +// @tags() (plural) is equivalent — use whichever reads more naturally +@reactor() +@tags('Notifications', 'Customer', 'Email') +class TaggingCustomerNotificationReactorAlternate {} ``` diff --git a/Documentation/client-snippets/concepts/tagging/observer-single-tag.md b/Documentation/client-snippets/concepts/tagging/observer-single-tag.md index 19b3207..66be54c 100644 --- a/Documentation/client-snippets/concepts/tagging/observer-single-tag.md +++ b/Documentation/client-snippets/concepts/tagging/observer-single-tag.md @@ -1,3 +1,7 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { reactor, tag } from '@cratis/chronicle'; + +@reactor() +@tag('Notifications') +class TaggingOrderConfirmationReactor {} ``` diff --git a/Documentation/client-snippets/concepts/tagging/read-model-tags.md b/Documentation/client-snippets/concepts/tagging/read-model-tags.md index 19b3207..131a6d3 100644 --- a/Documentation/client-snippets/concepts/tagging/read-model-tags.md +++ b/Documentation/client-snippets/concepts/tagging/read-model-tags.md @@ -1,3 +1,8 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { tag } from '@cratis/chronicle'; + +@tag('Reporting', 'Analytics') +class TaggingConceptsSalesReport { + constructor(readonly totalSales: number, readonly orderCount: number) {} +} ``` diff --git a/Documentation/client-snippets/concepts/tagging/static-event-tags.md b/Documentation/client-snippets/concepts/tagging/static-event-tags.md index 19b3207..bdfc140 100644 --- a/Documentation/client-snippets/concepts/tagging/static-event-tags.md +++ b/Documentation/client-snippets/concepts/tagging/static-event-tags.md @@ -1,3 +1,24 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { eventType, tag, tags } from '@cratis/chronicle'; + +@eventType() +@tag('analytics', 'user-action') +class TaggingUserLoggedIn { + constructor(readonly userId: string, readonly loggedInAt: Date) {} +} + +// @tags() (plural) is equivalent to @tag() — use whichever reads more naturally +@eventType() +@tags('analytics', 'user-action') +class TaggingUserLoggedInAlternate { + constructor(readonly userId: string, readonly loggedInAt: Date) {} +} + +// Mixing @tag() and @tags() on the same type merges all the tags +@eventType() +@tag('security') +@tags('audit') +class TaggingUserPasswordChanged { + constructor(readonly userId: string, readonly changedAt: Date) {} +} ``` diff --git a/Documentation/client-snippets/concepts/tagging/tags-in-event-context.md b/Documentation/client-snippets/concepts/tagging/tags-in-event-context.md index 19b3207..00aabfa 100644 --- a/Documentation/client-snippets/concepts/tagging/tags-in-event-context.md +++ b/Documentation/client-snippets/concepts/tagging/tags-in-event-context.md @@ -1,3 +1,27 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { EventContext, reducer } from '@cratis/chronicle'; + +class TaggingUserAnalytics { + loginCount = 0; + criticalLoginCount = 0; +} + +@reducer('', undefined, TaggingUserAnalytics) +class TaggingUserAnalyticsReducer { + taggingUserLoggedIn( + event: TaggingUserLoggedIn, + current: TaggingUserAnalytics | undefined, + context: EventContext + ): TaggingUserAnalytics { + const analytics = current ?? new TaggingUserAnalytics(); + + // Access tags from the event context + const isCritical = context.tags.some(tag => tag.value === 'critical'); + + return { + loginCount: analytics.loginCount + 1, + criticalLoginCount: analytics.criticalLoginCount + (isCritical ? 1 : 0) + }; + } +} ``` diff --git a/Documentation/client-snippets/events/appending-with-tags/append-many.md b/Documentation/client-snippets/events/appending-with-tags/append-many.md index 7943a87..b22272b 100644 --- a/Documentation/client-snippets/events/appending-with-tags/append-many.md +++ b/Documentation/client-snippets/events/appending-with-tags/append-many.md @@ -1,4 +1,26 @@ -```text -The TypeScript Chronicle client does not support this workflow yet. -Its append-many options do not currently expose dynamic event tags. +```typescript +import { eventType, EventForEventSourceId, IEventStore } from '@cratis/chronicle'; + +@eventType() +class TaggedMoneyWithdrawn { + constructor(readonly amount: number) {} +} + +@eventType() +class TaggedMoneyDeposited { + constructor(readonly amount: number) {} +} + +class TaggedTransferService { + constructor(private readonly store: IEventStore) {} + + async transfer(fromAccountId: string, toAccountId: string, amount: number): Promise { + const events: EventForEventSourceId[] = [ + { eventSourceId: fromAccountId, event: new TaggedMoneyWithdrawn(amount) }, + { eventSourceId: toAccountId, event: new TaggedMoneyDeposited(amount) } + ]; + + await this.store.eventLog.appendMany(events, { tags: ['transfer', 'audit'] }); + } +} ``` diff --git a/Documentation/client-snippets/events/appending-with-tags/append.md b/Documentation/client-snippets/events/appending-with-tags/append.md index 0515efe..01d5129 100644 --- a/Documentation/client-snippets/events/appending-with-tags/append.md +++ b/Documentation/client-snippets/events/appending-with-tags/append.md @@ -1,4 +1,19 @@ -```text -The TypeScript Chronicle client does not support this workflow yet. -Its append options do not currently expose dynamic event tags. +```typescript +import { eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class TaggedOrderPlaced { + constructor(readonly customerId: string, readonly total: number) {} +} + +class TaggedCheckoutService { + constructor(private readonly store: IEventStore) {} + + async placeOrder(orderId: string, customerId: string, total: number): Promise { + await this.store.eventLog.append( + orderId, + new TaggedOrderPlaced(customerId, total), + { tags: ['checkout', 'priority'] }); + } +} ``` diff --git a/Documentation/client-snippets/events/filtering/by-tag/multiple-filters.md b/Documentation/client-snippets/events/filtering/by-tag/multiple-filters.md index 19b3207..83c11fd 100644 --- a/Documentation/client-snippets/events/filtering/by-tag/multiple-filters.md +++ b/Documentation/client-snippets/events/filtering/by-tag/multiple-filters.md @@ -1,3 +1,15 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { eventType, filterEventsByTag, reactor } from '@cratis/chronicle'; + +@eventType() +class FilterByTagMultiCustomerRegistered { + constructor(readonly emailAddress: string) {} +} + +@reactor() +@filterEventsByTag('vip') +@filterEventsByTag('priority') +class FilterByTagMultiPriorityNotificationsReactor { + async filterByTagMultiCustomerRegistered(_event: FilterByTagMultiCustomerRegistered): Promise {} +} ``` diff --git a/Documentation/client-snippets/events/filtering/by-tag/reactor.md b/Documentation/client-snippets/events/filtering/by-tag/reactor.md index 19b3207..b5969f8 100644 --- a/Documentation/client-snippets/events/filtering/by-tag/reactor.md +++ b/Documentation/client-snippets/events/filtering/by-tag/reactor.md @@ -1,3 +1,26 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { EventContext, eventType, filterEventsByTag, IEventStore, reactor, tag } from '@cratis/chronicle'; + +@eventType() +@tag('customer-lifecycle') +class FilterByTagCustomerRegistered { + constructor(readonly emailAddress: string) {} +} + +class FilterByTagCustomerRegistrationService { + constructor(private readonly store: IEventStore) {} + + async register(eventSourceId: string, emailAddress: string): Promise { + await this.store.eventLog.append( + eventSourceId, + new FilterByTagCustomerRegistered(emailAddress), + { tags: ['vip', 'onboarding'] }); + } +} + +@reactor() +@filterEventsByTag('vip') +class FilterByTagVipWelcomeReactor { + async filterByTagCustomerRegistered(_event: FilterByTagCustomerRegistered, _context: EventContext): Promise {} +} ``` diff --git a/Documentation/client-snippets/events/filtering/by-tag/reducer.md b/Documentation/client-snippets/events/filtering/by-tag/reducer.md index 19b3207..ab1c52b 100644 --- a/Documentation/client-snippets/events/filtering/by-tag/reducer.md +++ b/Documentation/client-snippets/events/filtering/by-tag/reducer.md @@ -1,3 +1,34 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { eventType, filterEventsByTag, IEventStore, reducer } from '@cratis/chronicle'; + +@eventType() +class FilterByTagOrderPlaced { + constructor(readonly totalAmount: number) {} +} + +class FilterByTagPriorityOrderTotals { + totalAmount = 0; +} + +@reducer('', undefined, FilterByTagPriorityOrderTotals) +@filterEventsByTag('priority') +class FilterByTagPriorityOrderTotalsReducer { + filterByTagOrderPlaced( + event: FilterByTagOrderPlaced, + current: FilterByTagPriorityOrderTotals | undefined + ): FilterByTagPriorityOrderTotals { + return { totalAmount: (current?.totalAmount ?? 0) + event.totalAmount }; + } +} + +class FilterByTagCheckoutService { + constructor(private readonly store: IEventStore) {} + + async placePriorityOrder(eventSourceId: string, totalAmount: number): Promise { + await this.store.eventLog.append( + eventSourceId, + new FilterByTagOrderPlaced(totalAmount), + { tags: ['priority'] }); + } +} ``` diff --git a/Documentation/client-snippets/reactors/filtering/by-tag.md b/Documentation/client-snippets/reactors/filtering/by-tag.md index 19b3207..4bd602b 100644 --- a/Documentation/client-snippets/reactors/filtering/by-tag.md +++ b/Documentation/client-snippets/reactors/filtering/by-tag.md @@ -1,3 +1,25 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { EventContext, eventType, filterEventsByTag, IEventStore, reactor } from '@cratis/chronicle'; + +@eventType() +class ReactorsFilteringByTagOrderPlaced { + constructor(readonly totalAmount: number) {} +} + +class ReactorsFilteringByTagOrderService { + constructor(private readonly store: IEventStore) {} + + async placePriorityOrder(eventSourceId: string, totalAmount: number): Promise { + await this.store.eventLog.append( + eventSourceId, + new ReactorsFilteringByTagOrderPlaced(totalAmount), + { tags: ['priority'] }); + } +} + +@reactor() +@filterEventsByTag('priority') +class ReactorsFilteringPriorityOrderNotifier { + async reactorsFilteringByTagOrderPlaced(_event: ReactorsFilteringByTagOrderPlaced, _context: EventContext): Promise {} +} ``` diff --git a/Documentation/client-snippets/reactors/filtering/multiple-tags.md b/Documentation/client-snippets/reactors/filtering/multiple-tags.md index 19b3207..c0a8a80 100644 --- a/Documentation/client-snippets/reactors/filtering/multiple-tags.md +++ b/Documentation/client-snippets/reactors/filtering/multiple-tags.md @@ -1,3 +1,15 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { EventContext, eventType, filterEventsByTag, reactor } from '@cratis/chronicle'; + +@eventType() +class ReactorsFilteringMultiTagOrderPlaced { + constructor(readonly totalAmount: number) {} +} + +@reactor() +@filterEventsByTag('priority') +@filterEventsByTag('express') +class ReactorsFilteringFastTrackOrderNotifier { + async reactorsFilteringMultiTagOrderPlaced(_event: ReactorsFilteringMultiTagOrderPlaced, _context: EventContext): Promise {} +} ``` diff --git a/Documentation/client-snippets/reducers/filtering/by-tag.md b/Documentation/client-snippets/reducers/filtering/by-tag.md index 19b3207..799479d 100644 --- a/Documentation/client-snippets/reducers/filtering/by-tag.md +++ b/Documentation/client-snippets/reducers/filtering/by-tag.md @@ -1,3 +1,38 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { eventType, filterEventsByTag, IEventStore, reducer } from '@cratis/chronicle'; + +@eventType() +class ReducersFilteringByTagOrderPlaced { + constructor(readonly totalAmount: number) {} +} + +class ReducersFilteringPriorityOrderTotals { + count = 0; + total = 0; +} + +@reducer('', undefined, ReducersFilteringPriorityOrderTotals) +@filterEventsByTag('priority') +class ReducersFilteringPriorityOrderTotalsReducer { + reducersFilteringByTagOrderPlaced( + event: ReducersFilteringByTagOrderPlaced, + current: ReducersFilteringPriorityOrderTotals | undefined + ): ReducersFilteringPriorityOrderTotals { + return { + count: (current?.count ?? 0) + 1, + total: (current?.total ?? 0) + event.totalAmount + }; + } +} + +class ReducersFilteringByTagOrderService { + constructor(private readonly store: IEventStore) {} + + async placePriorityOrder(eventSourceId: string, totalAmount: number): Promise { + await this.store.eventLog.append( + eventSourceId, + new ReducersFilteringByTagOrderPlaced(totalAmount), + { tags: ['priority'] }); + } +} ``` diff --git a/Documentation/client-snippets/reducers/filtering/multiple-tags.md b/Documentation/client-snippets/reducers/filtering/multiple-tags.md index 19b3207..2a28635 100644 --- a/Documentation/client-snippets/reducers/filtering/multiple-tags.md +++ b/Documentation/client-snippets/reducers/filtering/multiple-tags.md @@ -1,3 +1,24 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { eventType, filterEventsByTag, reducer } from '@cratis/chronicle'; + +@eventType() +class ReducersFilteringMultiTagOrderPlaced { + constructor(readonly totalAmount: number) {} +} + +class ReducersFilteringFastTrackOrderTotals { + count = 0; +} + +@reducer('', undefined, ReducersFilteringFastTrackOrderTotals) +@filterEventsByTag('priority') +@filterEventsByTag('express') +class ReducersFilteringFastTrackOrderTotalsReducer { + reducersFilteringMultiTagOrderPlaced( + _event: ReducersFilteringMultiTagOrderPlaced, + current: ReducersFilteringFastTrackOrderTotals | undefined + ): ReducersFilteringFastTrackOrderTotals { + return { count: (current?.count ?? 0) + 1 }; + } +} ``` diff --git a/Documentation/client-snippets/reducers/tagging-reducers/mixed-approach.md b/Documentation/client-snippets/reducers/tagging-reducers/mixed-approach.md index 19b3207..d77e190 100644 --- a/Documentation/client-snippets/reducers/tagging-reducers/mixed-approach.md +++ b/Documentation/client-snippets/reducers/tagging-reducers/mixed-approach.md @@ -1,3 +1,12 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { reducer, tag } from '@cratis/chronicle'; + +class TaggingReducersExecutiveDashboard { + metricCount = 0; +} + +@reducer('', undefined, TaggingReducersExecutiveDashboard) +@tag('Analytics', 'Reporting') +@tag('Executive') +class TaggingReducersExecutiveDashboardReducer {} ``` diff --git a/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-multiple-attributes.md b/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-multiple-attributes.md index 19b3207..c9b2d22 100644 --- a/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-multiple-attributes.md +++ b/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-multiple-attributes.md @@ -1,3 +1,13 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { reducer, tag } from '@cratis/chronicle'; + +class TaggingReducersComplianceReport { + status = ''; +} + +@reducer('', undefined, TaggingReducersComplianceReport) +@tag('Analytics') +@tag('Compliance') +@tag('Auditing') +class TaggingReducersComplianceReportReducer {} ``` diff --git a/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-single-attribute.md b/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-single-attribute.md index 19b3207..07ba235 100644 --- a/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-single-attribute.md +++ b/Documentation/client-snippets/reducers/tagging-reducers/multiple-tags-single-attribute.md @@ -1,3 +1,11 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { reducer, tag } from '@cratis/chronicle'; + +class TaggingReducersSalesReport { + totalSales = 0; +} + +@reducer('', undefined, TaggingReducersSalesReport) +@tag('Analytics', 'Reporting', 'Dashboard') +class TaggingReducersSalesReportReducer {} ``` diff --git a/Documentation/client-snippets/reducers/tagging-reducers/single-tag.md b/Documentation/client-snippets/reducers/tagging-reducers/single-tag.md index 19b3207..b7ab98d 100644 --- a/Documentation/client-snippets/reducers/tagging-reducers/single-tag.md +++ b/Documentation/client-snippets/reducers/tagging-reducers/single-tag.md @@ -1,3 +1,27 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { eventType, reducer, tag } from '@cratis/chronicle'; + +@eventType() +class TaggingReducersOrderPlaced { + constructor(readonly totalAmount: number) {} +} + +class TaggingReducersOrderAnalytics { + orderCount = 0; + totalAmount = 0; +} + +@reducer('', undefined, TaggingReducersOrderAnalytics) +@tag('Analytics') +class TaggingReducersOrderAnalyticsReducer { + taggingReducersOrderPlaced( + event: TaggingReducersOrderPlaced, + current: TaggingReducersOrderAnalytics | undefined + ): TaggingReducersOrderAnalytics { + return { + orderCount: (current?.orderCount ?? 0) + 1, + totalAmount: (current?.totalAmount ?? 0) + event.totalAmount + }; + } +} ``` diff --git a/Documentation/client-snippets/reducers/tagging-reducers/tag-categories.md b/Documentation/client-snippets/reducers/tagging-reducers/tag-categories.md index 19b3207..39f27cb 100644 --- a/Documentation/client-snippets/reducers/tagging-reducers/tag-categories.md +++ b/Documentation/client-snippets/reducers/tagging-reducers/tag-categories.md @@ -1,3 +1,18 @@ -```text -TypeScript does not support this workflow yet. +```typescript +import { reducer, tag } from '@cratis/chronicle'; + +class TaggingReducersCategoryExamples { + id = ''; +} + +@reducer('', undefined, TaggingReducersCategoryExamples) +// By domain +@tag('Sales', 'Inventory', 'Customer') +// By purpose +@tag('Analytics', 'Reporting', 'Dashboard', 'Auditing') +// By stakeholder +@tag('Executive', 'Operations', 'Finance') +// By data type +@tag('Aggregates', 'Summaries', 'Metrics') +class TaggingReducersCategoryExamplesReducer {} ``` diff --git a/Source/eventSequences/AppendOptions.ts b/Source/eventSequences/AppendOptions.ts index a6ab848..bf0d782 100644 --- a/Source/eventSequences/AppendOptions.ts +++ b/Source/eventSequences/AppendOptions.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { Guid } from '@cratis/fundamentals'; +import type { Tag } from '../events/Tag'; import type { ConcurrencyScope } from './ConcurrencyScope'; /** @@ -17,6 +18,14 @@ export interface AppendOptions { /** Optional concurrency scope to use for append operations. */ concurrencyScope?: ConcurrencyScope; + /** + * Optional tags to associate with the event(s) being appended. These are combined with any + * static tags declared on the event type(s) via `@tag()`/`@tags()`, and - for the + * `appendMany(events: EventForEventSourceId[], options?)` overload - with any tags carried + * by the individual {@link EventForEventSourceId} entries. + */ + tags?: ReadonlyArray; + /** * Optional per-event-source-id concurrency scopes, keyed by event source id. * Only meaningful for the `appendMany(events: EventForEventSourceId[], options?)` overload, which diff --git a/Source/eventSequences/EventForEventSourceId.ts b/Source/eventSequences/EventForEventSourceId.ts index d885531..728d0c5 100644 --- a/Source/eventSequences/EventForEventSourceId.ts +++ b/Source/eventSequences/EventForEventSourceId.ts @@ -1,6 +1,8 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +import type { Tag } from '../events/Tag'; + /** * Represents an event paired with the event source identifier it belongs to. */ @@ -22,4 +24,11 @@ export interface EventForEventSourceId { /** Optional subject identifying the target the event is about. Defaults to the event source id. */ readonly subject?: string; + + /** + * Optional tags to associate with the event. These are combined with any static tags + * declared on the event type via `@tag()`/`@tags()`, and with any tags supplied at + * append time. + */ + readonly tags?: ReadonlyArray; } diff --git a/Source/eventSequences/EventSequence.ts b/Source/eventSequences/EventSequence.ts index 2652c5d..a01f134 100644 --- a/Source/eventSequences/EventSequence.ts +++ b/Source/eventSequences/EventSequence.ts @@ -10,6 +10,9 @@ import type { AppendedEvent } from '../events/AppendedEvent'; import { EventType } from '../events/EventType'; import { EventTypeId } from '../events/EventTypeId'; import { EventTypeGeneration } from '../events/EventTypeGeneration'; +import { Tag } from '../events/Tag'; +import { getTagsFor } from '../events/tagDecorator'; +import { mergeTags } from '../events/mergeTags'; import { DecoratorType } from '../types/DecoratorType'; import { TypeDiscoverer } from '../types/TypeDiscoverer'; import { toClientFailedPartition } from '../observation/toClientFailedPartition'; @@ -66,6 +69,9 @@ export class EventSequence implements IEventSequence { : Guid.as(options.correlationId); const content = JsonSerializer.serialize(event); + // Merge static tags declared on the event type with tags supplied at append time. + const tags = mergeTags(getTagsFor(event.constructor as Function), options?.tags); + causationManager.add(CausationType.appendEvent, { eventType: eventType.id.value }); const causationChain = causationManager.getCurrentChain(); const identity = identityProvider.getCurrent(); @@ -108,7 +114,7 @@ export class EventSequence implements IEventSequence { })), CausedBy: toContractsCausedBy(identity), ConcurrencyScope: this.toContractConcurrencyScope(options?.concurrencyScope), - Tags: [], + Tags: tags, Occurred: undefined, Subject: eventSourceId }); @@ -149,7 +155,8 @@ export class EventSequence implements IEventSequence { eventType, occurred: new Date(), correlationId: correlationId.toString(), - causation: causationChain.map(c => ({ type: c.type.name, properties: { ...c.properties } })) + causation: causationChain.map(c => ({ type: c.type.name, properties: { ...c.properties } })), + tags: tags.map(value => new Tag(value)) }, eventType, content: event as Record @@ -223,8 +230,13 @@ export class EventSequence implements IEventSequence { const resolveConcurrencyScope = (eventSourceId: string) => this.toContractConcurrencyScope(concurrencyScopesByEventSourceId?.[eventSourceId] ?? defaultConcurrencyScope); - const eventsToAppend = eventsForEventSourceIds.map(({ eventSourceId, event, eventStreamType, eventStreamId, eventSourceType, subject }) => { + const eventsToAppend = eventsForEventSourceIds.map(({ eventSourceId, event, eventStreamType, eventStreamId, eventSourceType, subject, tags: instanceTags }) => { const eventType = getEventTypeFor(event.constructor as Function); + + // Merge static tags declared on the event type, tags carried by this specific + // EventForEventSourceId entry, and tags supplied at call time for the whole batch. + const tags = mergeTags(getTagsFor(event.constructor as Function), instanceTags, appendOptions?.tags); + return { EventSourceType: eventSourceType ?? 'Default', EventSourceId: eventSourceId, @@ -242,7 +254,7 @@ export class EventSequence implements IEventSequence { Properties: { ...c.properties } })), CausedBy: toContractsCausedBy(identity), - Tags: [], + Tags: tags, Occurred: undefined, Subject: subject ?? eventSourceId }; @@ -334,7 +346,8 @@ export class EventSequence implements IEventSequence { eventType, occurred: occurredAt, correlationId: correlationId.toString(), - causation: causationEntries + causation: causationEntries, + tags: eventsToAppend[index].Tags.map(value => new Tag(value)) }, eventType, content: event as Record @@ -687,7 +700,8 @@ export class EventSequence implements IEventSequence { causation: (context.Causation ?? []).map(c => ({ type: c.Type, properties: { ...c.Properties } - })) + })), + tags: (context.Tags ?? []).map(value => new Tag(value)) }, eventType, content: JSON.parse(wireEvent.Content) as Record diff --git a/Source/events/EventContext.ts b/Source/events/EventContext.ts index cc06489..9b74d45 100644 --- a/Source/events/EventContext.ts +++ b/Source/events/EventContext.ts @@ -3,6 +3,7 @@ import { EventType } from './EventType'; import { CausationEntry } from './CausationEntry'; +import { Tag } from './Tag'; /** * Represents contextual information about an appended event. @@ -25,4 +26,7 @@ export interface EventContext { /** The causation chain for the event. */ readonly causation: ReadonlyArray; + + /** The tags the event carries. */ + readonly tags: ReadonlyArray; } diff --git a/Source/events/Tag.ts b/Source/events/Tag.ts new file mode 100644 index 0000000..98e35fc --- /dev/null +++ b/Source/events/Tag.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/** + * Represents a tag associated with an event, observer, or read model. + */ +export class Tag { + constructor(readonly value: string) {} + + /** @inheritdoc */ + toString(): string { + return this.value; + } +} diff --git a/Source/events/filterEventsByTagDecorator.ts b/Source/events/filterEventsByTagDecorator.ts new file mode 100644 index 0000000..1c7838d --- /dev/null +++ b/Source/events/filterEventsByTagDecorator.ts @@ -0,0 +1,51 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { Tag } from './Tag'; +import { mergeTags } from './mergeTags'; + +/** Metadata key used to store the tags a reactor or reducer filters events by. */ +const FILTER_TAGS_METADATA_KEY = 'chronicle:filterEventsByTag'; + +/** + * TypeScript decorator that restricts a reactor or reducer so that it only handles events + * that carry a specific tag. This is the TypeScript equivalent of the C# `[FilterEventsByTag]` + * attribute. + * + * Apply this decorator to a `@reactor()` or `@reducer()`-decorated class to filter the observed + * event stream to events tagged with the given value. Use {@link tag}/{@link tags} when the + * intent is to *label* an observer for discoverability; use this decorator when the intent is + * to *filter* which events reach it. Applying the decorator more than once accumulates filter + * tags - a class handles events carrying any of the applied tags. + * + * @param value - The tag value that an event must carry in order to be dispatched to the observer. + * @returns A class decorator. + * + * @example + * ```typescript + * @reactor() + * @filterEventsByTag('vip') + * class VipWelcomeReactor { + * async customerRegistered(event: CustomerRegistered): Promise { + * console.log(`Welcome VIP customer ${event.emailAddress}`); + * } + * } + * ``` + */ +export function filterEventsByTag(value: string): ClassDecorator { + return (target: object) => { + const existing = (Reflect.getMetadata(FILTER_TAGS_METADATA_KEY, target) as Tag[] | undefined) ?? []; + const merged = mergeTags(existing, [value]).map(tagValue => new Tag(tagValue)); + Reflect.defineMetadata(FILTER_TAGS_METADATA_KEY, merged, target); + }; +} + +/** + * Gets all filter tags applied to a class via {@link filterEventsByTag}. + * @param target - The class constructor to retrieve filter tags for. + * @returns The filter tags applied to the class, or an empty array if none are. + */ +export function getFilterTagsFor(target: Function): Tag[] { + return (Reflect.getMetadata(FILTER_TAGS_METADATA_KEY, target) as Tag[] | undefined) ?? []; +} diff --git a/Source/events/index.ts b/Source/events/index.ts index 6d462d2..a102210 100644 --- a/Source/events/index.ts +++ b/Source/events/index.ts @@ -11,5 +11,9 @@ export type { CausationEntry } from './CausationEntry'; export type { AppendedEvent } from './AppendedEvent'; export type { IEventTypes } from './IEventTypes'; export { EventTypes } from './EventTypes'; +export { Tag } from './Tag'; +export { tag, tags, getTagsFor } from './tagDecorator'; +export { filterEventsByTag, getFilterTagsFor } from './filterEventsByTagDecorator'; +export { mergeTags } from './mergeTags'; export * from './constraints'; export * from './migrations'; diff --git a/Source/events/mergeTags.ts b/Source/events/mergeTags.ts new file mode 100644 index 0000000..55cb451 --- /dev/null +++ b/Source/events/mergeTags.ts @@ -0,0 +1,27 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { Tag } from './Tag'; + +/** + * Merges one or more sources of tags - static tags declared on a type, tags carried by an + * individual event instance, and tags supplied at call time - into a single, distinct list of + * tag values. Mirrors the .NET client, which merges static, instance, and dynamic tags the + * same way before sending an append request. + * @param sources - The tag sources to merge. Each may be a mix of strings and {@link Tag} instances, + * or undefined when that source contributed no tags. + * @returns The distinct, merged tag values. + */ +export function mergeTags(...sources: ReadonlyArray | undefined>): string[] { + const values = new Set(); + for (const source of sources) { + if (!source) continue; + for (const entry of source) { + const value = typeof entry === 'string' ? entry : entry.value; + if (value.trim().length > 0) { + values.add(value); + } + } + } + return [...values]; +} diff --git a/Source/events/tagDecorator.ts b/Source/events/tagDecorator.ts new file mode 100644 index 0000000..56f2aeb --- /dev/null +++ b/Source/events/tagDecorator.ts @@ -0,0 +1,59 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import 'reflect-metadata'; +import { Tag } from './Tag'; +import { mergeTags } from './mergeTags'; + +/** Metadata key used to store the tags labeling a class. */ +const TAGS_METADATA_KEY = 'chronicle:tags'; + +/** + * TypeScript decorator that labels an event type, reactor, or reducer with one or more tags. + * This is the TypeScript equivalent of the C# `[Tag]` attribute. + * + * Applying the decorator more than once accumulates tags rather than replacing them, so + * `@tag('a')` followed by `@tag('b')` on the same class results in both `'a'` and `'b'`. + * + * @param values - The tags to apply. + * @returns A class decorator. + * + * @example + * ```typescript + * @eventType() + * @tag('analytics', 'user-action') + * class UserLoggedIn { + * constructor(readonly userId: string) {} + * } + * ``` + */ +export function tag(...values: string[]): ClassDecorator { + return (target: object) => addTags(target, values); +} + +/** + * TypeScript decorator that labels an event type, reactor, or reducer with one or more tags. + * This is the TypeScript equivalent of the C# `[Tags]` attribute, and behaves identically to + * {@link tag} - use whichever reads more naturally at the call site. + * + * @param values - The tags to apply. + * @returns A class decorator. + */ +export function tags(...values: string[]): ClassDecorator { + return (target: object) => addTags(target, values); +} + +function addTags(target: object, values: string[]): void { + const existing = (Reflect.getMetadata(TAGS_METADATA_KEY, target) as Tag[] | undefined) ?? []; + const merged = mergeTags(existing, values).map(value => new Tag(value)); + Reflect.defineMetadata(TAGS_METADATA_KEY, merged, target); +} + +/** + * Gets all tags applied to a class via {@link tag} or {@link tags}. + * @param target - The class constructor to retrieve tags for. + * @returns The tags applied to the class, or an empty array if none are. + */ +export function getTagsFor(target: Function): Tag[] { + return (Reflect.getMetadata(TAGS_METADATA_KEY, target) as Tag[] | undefined) ?? []; +} diff --git a/Source/reactors/Reactors.ts b/Source/reactors/Reactors.ts index 00d000d..601002d 100644 --- a/Source/reactors/Reactors.ts +++ b/Source/reactors/Reactors.ts @@ -12,6 +12,9 @@ import { getEventTypeMetadata } from '../events/eventTypeDecorator'; import { EventContext } from '../events/EventContext'; import { EventTypeId } from '../events/EventTypeId'; import { EventTypeGeneration } from '../events/EventTypeGeneration'; +import { Tag } from '../events/Tag'; +import { getTagsFor } from '../events/tagDecorator'; +import { getFilterTagsFor } from '../events/filterEventsByTagDecorator'; import { EventSequenceId } from '../eventSequences/EventSequenceId'; import type { IEventLog } from '../eventSequences/IEventLog'; import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle'; @@ -239,9 +242,9 @@ export class Reactors implements IReactors { Key: EVENT_SOURCE_ID_KEY })), IsReplayable: false, - Tags: [], + Tags: getTagsFor(reactorType).map(t => t.value), Filters: { - FilterTags: [], + FilterTags: getFilterTagsFor(reactorType).map(t => t.value), EventSourceType: '', EventStreamType: 'All' } @@ -303,7 +306,8 @@ export class Reactors implements IReactors { }, occurred: new Date(event.Context!.Occurred?.Value ?? ''), correlationId: event.Context?.CorrelationId ? `${event.Context.CorrelationId.lo}-${event.Context.CorrelationId.hi}` : '', - causation: [] + causation: [], + tags: (event.Context!.Tags ?? []).map(value => new Tag(value)) }; this._logger.info('Invoking reactor handler', { diff --git a/Source/reducers/Reducers.spec.ts b/Source/reducers/Reducers.spec.ts index 42fc734..892f947 100644 --- a/Source/reducers/Reducers.spec.ts +++ b/Source/reducers/Reducers.spec.ts @@ -6,11 +6,48 @@ import type { Constructor } from '@cratis/fundamentals'; import type { IClientArtifactsProvider } from '../artifacts'; import type { ChronicleConnection } from '../connection'; import { ConnectionLifecycle } from '../connection/ConnectionLifecycle'; +import { eventType, getEventTypeFor } from '../events/eventTypeDecorator'; +import type { EventContext } from '../events/EventContext'; +import { filterEventsByTag } from '../events/filterEventsByTagDecorator'; +import { tag } from '../events/tagDecorator'; import { reducer } from './reducer'; import { Reducers } from './Reducers'; const flush = () => new Promise(resolve => setTimeout(resolve, 0)); +class ReducersSomeEventHappened { + constructor(readonly value: string = '') {} +} +eventType('d2b2b2b2-2222-4a3c-9d3f-6f2f4a3c9d3f')(ReducersSomeEventHappened); + +class SomeTaggedReducerState { + count = 0; +} + +class SomeTaggedReducer { + reducersSomeEventHappened(): SomeTaggedReducerState { + return { count: 1 }; + } +} +reducer('some-tagged-reducer', undefined, SomeTaggedReducerState)(SomeTaggedReducer); +tag('Analytics', 'Reporting')(SomeTaggedReducer); +filterEventsByTag('vip')(SomeTaggedReducer); +filterEventsByTag('priority')(SomeTaggedReducer); + +const receivedContexts: EventContext[] = []; + +class CapturingReducerState { + count = 0; +} + +class CapturingReducer { + reducersSomeEventHappened(_event: ReducersSomeEventHappened, current: CapturingReducerState | undefined, context: EventContext): CapturingReducerState { + receivedContexts.push(context); + return { count: (current?.count ?? 0) + 1 }; + } +} +reducer('capturing-reducer', undefined, CapturingReducerState)(CapturingReducer); + /** * A minimal client artifacts provider exposing only the reducer types under test — * Reducers.ts only reads .reducers and .eventTypes off this during discovery. diff --git a/Source/reducers/Reducers.ts b/Source/reducers/Reducers.ts index 6571f3b..23462fb 100644 --- a/Source/reducers/Reducers.ts +++ b/Source/reducers/Reducers.ts @@ -10,6 +10,12 @@ import { ChronicleConnection } from '../connection'; import { toContractsGuid } from '../connection/Guid'; import { ConnectionLifecycle } from '../connection/ConnectionLifecycle'; import { getEventTypeMetadata } from '../events/eventTypeDecorator'; +import { EventContext } from '../events/EventContext'; +import { EventTypeId } from '../events/EventTypeId'; +import { EventTypeGeneration } from '../events/EventTypeGeneration'; +import { Tag } from '../events/Tag'; +import { getTagsFor } from '../events/tagDecorator'; +import { getFilterTagsFor } from '../events/filterEventsByTagDecorator'; import { EventSequenceId } from '../eventSequences/EventSequenceId'; import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle'; import { IReducers } from './IReducers'; @@ -310,9 +316,9 @@ export class Reducers implements IReducers { })), ReadModel: readModelName, IsActive: isActive, - Tags: [], + Tags: getTagsFor(reducerType).map(t => t.value), Filters: { - FilterTags: [], + FilterTags: getFilterTagsFor(reducerType).map(t => t.value), EventSourceType: '', EventStreamType: 'All' } @@ -369,6 +375,19 @@ export class Reducers implements IReducers { } const content = JSON.parse(event.Content) as Record; + const context: EventContext = { + sequenceNumber: event.Context!.SequenceNumber, + eventSourceId: event.Context!.EventSourceId, + eventType: { + id: new EventTypeId(event.Context!.EventType!.Id), + generation: new EventTypeGeneration(event.Context!.EventType!.Generation), + tombstone: event.Context!.EventType!.Tombstone + }, + occurred: new Date(event.Context!.Occurred?.Value ?? ''), + correlationId: event.Context?.CorrelationId ? `${event.Context.CorrelationId.lo}-${event.Context.CorrelationId.hi}` : '', + causation: [], + tags: (event.Context!.Tags ?? []).map(value => new Tag(value)) + }; this._logger.info('Invoking reducer handler', { reducerId: id, @@ -378,7 +397,7 @@ export class Reducers implements IReducers { hasState: currentState !== undefined }); - currentState = await reducerInstance[entry.methodName](content, currentState); + currentState = await reducerInstance[entry.methodName](content, currentState, context); lastSuccessfullyObservedEvent = event.Context!.SequenceNumber; } catch (err) { this._logger.error('Error handling event in reducer', { reducerId: id, error: String(err) }); diff --git a/Source/seeding/EventSeeding.ts b/Source/seeding/EventSeeding.ts index 1d9afa9..e0a538c 100644 --- a/Source/seeding/EventSeeding.ts +++ b/Source/seeding/EventSeeding.ts @@ -6,6 +6,7 @@ import { IClientArtifactsProvider } from '../artifacts'; import { ChronicleConnection } from '../connection'; import { EventStoreNamespaceName } from '../EventStoreNamespaceName'; import { getEventTypeFor } from '../events/eventTypeDecorator'; +import { getTagsFor } from '../events/tagDecorator'; import { ICanSeedEvents } from './ICanSeedEvents'; import { IEventSeeding } from './IEventSeeding'; import { IEventSeedingBuilder } from './IEventSeedingBuilder'; @@ -104,11 +105,12 @@ export class EventSeeding implements IEventSeeding { private addEntries(eventSourceId: string, events: Iterable, isGlobal: boolean, targetNamespace: string): void { for (const event of events) { const eventType = getEventTypeFor(event.constructor as Function); + const tags = getTagsFor(event.constructor as Function).map(t => t.value); this._entries.push({ eventSourceId, eventTypeId: eventType.id.value, content: JSON.stringify(event), - tags: [], + tags, isGlobal, targetNamespace }); From 2b0e1573a7757373500afa27b52200d0c6b79341 Mon Sep 17 00:00:00 2001 From: Einar Date: Tue, 1 Sep 2026 20:50:18 +0200 Subject: [PATCH 39/39] Add specs for event tags Covers tag merge semantics, the tag/tags/filterEventsByTag decorators, and that both reach a reducer registration where Tags and FilterTags were previously hardcoded empty. --- Source/events/mergeTags.spec.ts | 50 +++++++++++++ Source/events/tagDecorator.spec.ts | 115 +++++++++++++++++++++++++++++ Source/reducers/Reducers.spec.ts | 43 +++++++++++ 3 files changed, 208 insertions(+) create mode 100644 Source/events/mergeTags.spec.ts create mode 100644 Source/events/tagDecorator.spec.ts diff --git a/Source/events/mergeTags.spec.ts b/Source/events/mergeTags.spec.ts new file mode 100644 index 0000000..5622204 --- /dev/null +++ b/Source/events/mergeTags.spec.ts @@ -0,0 +1,50 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { describe, expect, it } from 'vitest'; +import { Tag } from './Tag'; +import { mergeTags } from './mergeTags'; + +describe('mergeTags', () => { + describe('when merging a single source of strings', () => { + it('should return the values unchanged', () => { + expect(mergeTags(['a', 'b'])).toEqual(['a', 'b']); + }); + }); + + describe('when merging several sources', () => { + it('should combine them in order', () => { + expect(mergeTags(['a'], ['b'], ['c'])).toEqual(['a', 'b', 'c']); + }); + }); + + describe('when the same value appears in more than one source', () => { + it('should keep only the first occurrence', () => { + expect(mergeTags(['a', 'b'], ['b', 'c'])).toEqual(['a', 'b', 'c']); + }); + }); + + describe('when a source holds Tag instances', () => { + it('should unwrap them to their values', () => { + expect(mergeTags([new Tag('a')], ['b'])).toEqual(['a', 'b']); + }); + }); + + describe('when a source is undefined', () => { + it('should skip it', () => { + expect(mergeTags(['a'], undefined, ['b'])).toEqual(['a', 'b']); + }); + }); + + describe('when a value is blank', () => { + it('should drop it, so an empty tag never reaches the wire', () => { + expect(mergeTags(['a', '', ' '])).toEqual(['a']); + }); + }); + + describe('when there are no sources at all', () => { + it('should return an empty list', () => { + expect(mergeTags()).toEqual([]); + }); + }); +}); diff --git a/Source/events/tagDecorator.spec.ts b/Source/events/tagDecorator.spec.ts new file mode 100644 index 0000000..bf4ba31 --- /dev/null +++ b/Source/events/tagDecorator.spec.ts @@ -0,0 +1,115 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { describe, expect, it } from 'vitest'; +import { getTagsFor, tag, tags } from './tagDecorator'; +import { filterEventsByTag, getFilterTagsFor } from './filterEventsByTagDecorator'; + +describe('tag', () => { + describe('when a class carries no tags', () => { + class Untagged {} + + it('should report no tags', () => { + expect(getTagsFor(Untagged)).toEqual([]); + }); + }); + + describe('when decorating a class with a single tag', () => { + class SingleTagged {} + tag('analytics')(SingleTagged); + + it('should carry that tag', () => { + expect(getTagsFor(SingleTagged).map(_ => _.value)).toEqual(['analytics']); + }); + }); + + describe('when decorating a class with several tags at once', () => { + class MultiTagged {} + tag('analytics', 'user-action')(MultiTagged); + + it('should carry every tag', () => { + expect(getTagsFor(MultiTagged).map(_ => _.value)).toEqual(['analytics', 'user-action']); + }); + }); + + describe('when applying the decorator more than once', () => { + class Accumulated {} + tag('first')(Accumulated); + tag('second')(Accumulated); + + it('should accumulate rather than replace', () => { + expect(getTagsFor(Accumulated).map(_ => _.value)).toEqual(['first', 'second']); + }); + }); + + describe('when the same tag is applied twice', () => { + class Duplicated {} + tag('same')(Duplicated); + tag('same')(Duplicated); + + it('should keep it once', () => { + expect(getTagsFor(Duplicated).map(_ => _.value)).toEqual(['same']); + }); + }); + + describe('when using the plural tags decorator', () => { + class PluralTagged {} + tags('a', 'b')(PluralTagged); + + it('should behave identically to tag', () => { + expect(getTagsFor(PluralTagged).map(_ => _.value)).toEqual(['a', 'b']); + }); + }); + + describe('when two classes are tagged separately', () => { + class FirstTagged {} + class SecondTagged {} + tag('one')(FirstTagged); + tag('two')(SecondTagged); + + it('should not leak tags between them', () => { + expect(getTagsFor(FirstTagged).map(_ => _.value)).toEqual(['one']); + expect(getTagsFor(SecondTagged).map(_ => _.value)).toEqual(['two']); + }); + }); +}); + +describe('filterEventsByTag', () => { + describe('when a class carries no filter', () => { + class Unfiltered {} + + it('should report no filter tags', () => { + expect(getFilterTagsFor(Unfiltered)).toEqual([]); + }); + }); + + describe('when decorating a class with a filter tag', () => { + class Filtered {} + filterEventsByTag('vip')(Filtered); + + it('should carry that filter tag', () => { + expect(getFilterTagsFor(Filtered).map(_ => _.value)).toEqual(['vip']); + }); + }); + + describe('when applying the decorator more than once', () => { + class MultiFiltered {} + filterEventsByTag('vip')(MultiFiltered); + filterEventsByTag('premium')(MultiFiltered); + + it('should accumulate, so the observer handles events carrying any of them', () => { + expect(getFilterTagsFor(MultiFiltered).map(_ => _.value)).toEqual(['vip', 'premium']); + }); + }); + + describe('when a class is both labeled and filtered', () => { + class LabeledAndFiltered {} + tag('reporting')(LabeledAndFiltered); + filterEventsByTag('vip')(LabeledAndFiltered); + + it('should keep labeling and filtering separate', () => { + expect(getTagsFor(LabeledAndFiltered).map(_ => _.value)).toEqual(['reporting']); + expect(getFilterTagsFor(LabeledAndFiltered).map(_ => _.value)).toEqual(['vip']); + }); + }); +}); diff --git a/Source/reducers/Reducers.spec.ts b/Source/reducers/Reducers.spec.ts index 892f947..2a1876f 100644 --- a/Source/reducers/Reducers.spec.ts +++ b/Source/reducers/Reducers.spec.ts @@ -121,4 +121,47 @@ describe('Reducers', () => { expect(message.Content.Value0.Reducer.IsActive).toBe(false); }); }); + + describe('when registering a reducer that is tagged and filters by tag', () => { + type TaggedRegistration = { + Content: { Value0: { Reducer: { Tags: string[]; Filters: { FilterTags: string[] } } } }; + }; + + const register = async () => { + const { connection, registrationMessages } = createConnection(); + const reducers = new Reducers(createArtifacts([SomeTaggedReducer]), connection, 'my-event-store', 'my-namespace', new ConnectionLifecycle(), 'default-sink'); + + await reducers.register(); + await flush(); + + return (registrationMessages[0] as TaggedRegistration).Content.Value0.Reducer; + }; + + it('should carry the tags the reducer is labeled with', async () => { + expect((await register()).Tags).toEqual(['Analytics', 'Reporting']); + }); + + it('should carry every tag it filters events by', async () => { + expect((await register()).Filters.FilterTags).toEqual(['vip', 'priority']); + }); + }); + + describe('when registering a reducer that is neither tagged nor filtered', () => { + class SomeUntaggedReducer {} + reducer('some-untagged-reducer')(SomeUntaggedReducer); + + it('should send empty tag lists rather than omitting them', async () => { + const { connection, registrationMessages } = createConnection(); + const reducers = new Reducers(createArtifacts([SomeUntaggedReducer]), connection, 'my-event-store', 'my-namespace', new ConnectionLifecycle(), 'default-sink'); + + await reducers.register(); + await flush(); + + const message = registrationMessages[0] as { + Content: { Value0: { Reducer: { Tags: string[]; Filters: { FilterTags: string[] } } } }; + }; + expect(message.Content.Value0.Reducer.Tags).toEqual([]); + expect(message.Content.Value0.Reducer.Filters.FilterTags).toEqual([]); + }); + }); });