diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 62c9d465..b770095c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,7 +77,7 @@ A hosted run does not replace local verification. The hosted CI workflow also su ### Upstream declaration errors -The core, host adapters, MongoDB, testing, proxy generator, and ESLint plugin consumer files use `skipLibCheck: false`. Chronicle and Drizzle consumer files use `skipLibCheck: true` **only** for third-party declaration errors; the script first runs their NodeNext compilation with `skipLibCheck: false`, prints the upstream diagnostics, and rejects errors in Arc declarations or consumer code. The pinned `@cratis/chronicle@6.35.0` and `@cratis/chronicle.contracts@19.26.2` declarations no longer require an exception; any Chronicle declaration error fails the guard. `drizzle-orm@0.45.3` has `gel-core/columns/date-duration.d.ts(1,35)` TS2307 (missing `gel`) and `pg-core/query-builders/query.d.ts(23,22)` TS2420 (`PgRelationalQuery` lacks `getSQL`), among other internal declaration errors. Fix these in their owning packages before removing the temporary integration exception. +The core, host adapters, MongoDB, testing, proxy generator, and ESLint plugin consumer files use `skipLibCheck: false`. Chronicle and Drizzle consumer files use `skipLibCheck: true` **only** for third-party declaration errors; the script first runs their NodeNext compilation with `skipLibCheck: false`, prints the upstream diagnostics, and rejects errors in Arc declarations or consumer code. The pinned `@cratis/chronicle@6.49.0` and `@cratis/chronicle.contracts@19.26.2` declarations no longer require an exception; any Chronicle declaration error fails the guard. `drizzle-orm@0.45.3` has `gel-core/columns/date-duration.d.ts(1,35)` TS2307 (missing `gel`) and `pg-core/query-builders/query.d.ts(23,22)` TS2420 (`PgRelationalQuery` lacks `getSQL`), among other internal declaration errors. Fix these in their owning packages before removing the temporary integration exception. ## Conventions diff --git a/Documentation/chronicle/add-event-sourcing.md b/Documentation/chronicle/add-event-sourcing.md index 52eabbec..057d8d78 100644 --- a/Documentation/chronicle/add-event-sourcing.md +++ b/Documentation/chronicle/add-event-sourcing.md @@ -57,7 +57,7 @@ cd ../my-arc-app Install it together with the Chronicle SDK and RxJS: ```bash -npm install ../arc-packages/arc.chronicle.tgz @cratis/chronicle@~6.35.0 rxjs@^7.8.2 +npm install ../arc-packages/arc.chronicle.tgz @cratis/chronicle@~6.49.0 rxjs@^7.8.2 ``` | Package | What it gives you | diff --git a/Documentation/chronicle/aggregates/injecting-into-commands.md b/Documentation/chronicle/aggregates/injecting-into-commands.md index c47e3b18..45e9fd9a 100644 --- a/Documentation/chronicle/aggregates/injecting-into-commands.md +++ b/Documentation/chronicle/aggregates/injecting-into-commands.md @@ -64,7 +64,7 @@ You may `return order.commit()` to make the commit visible. Do **not** also retu The aggregate belongs to the command's key: the `@key()` field, `getKey()`, or `getEventSourceId()`. A command without a key fails with the exception `A command key is required for Order` before `handle()` runs. -Loading uses the same route the command's returned events use: the current tenant's namespace, the command's `@eventSourceType`, `@eventStreamType`, and its stream ID from `getEventStreamId()` or `@eventStreamId`. Arc reads the events of the types the aggregate handles, in order, and replays them. See [Event metadata](../commands/event-metadata.md). +Loading uses the same route the command's returned events use: the current tenant's namespace, the command's `@eventSourceType`, `@eventStreamType`, and its stream ID from `getEventStreamId()` or `@eventStreamId`. Arc reads the events of the types the aggregate handles, in order, and replays them. See [Event metadata](../commands/event-metadata.md). An aggregate can instead declare an [event source definition](../commands/event-source-definitions.md), which guards and rehydrates only that source and stream. Arc loads each aggregate type once per command. A second parameter of the same type receives the same instance. For a second aggregate **type** on the same key, bind another `commandAggregate(Type)`. There is no way to load an aggregate for a different ID; the key decides. diff --git a/Documentation/chronicle/commands/event-metadata.md b/Documentation/chronicle/commands/event-metadata.md index f763156c..57e9b07c 100644 --- a/Documentation/chronicle/commands/event-metadata.md +++ b/Documentation/chronicle/commands/event-metadata.md @@ -20,6 +20,8 @@ An event records more than its payload. It also records which entity it belongs | Caused by | The signed-in principal, or Chronicle's system identity for an anonymous caller | None | | Causation | An `Arc.Command` entry with the command name and its values; see [Causation and auditing](causation.md) | None | +To select a registered Chronicle event source definition instead of free-form strings, see [Event source definitions](event-source-definitions.md). + Routing decorators come from `@cratis/arc.chronicle` and apply to every event the command returns. A value set on an `eventForEventSourceId` entry wins over the command's default for that entry only. ## Set command-wide defaults @@ -90,4 +92,5 @@ export class Onboarding { - [Returning events](index.md) - [Resolving the event source ID](../resolving-event-source-id.md) +- [Event source definitions](event-source-definitions.md) - [Concurrency](concurrency.md), where the same routing decorators opt into tail checks diff --git a/Documentation/chronicle/commands/event-source-definitions.md b/Documentation/chronicle/commands/event-source-definitions.md new file mode 100644 index 00000000..70a5f5b9 --- /dev/null +++ b/Documentation/chronicle/commands/event-source-definitions.md @@ -0,0 +1,89 @@ +--- +title: Event source definitions +description: Route a command's events, and an aggregate's, through a Chronicle event source definition and stream, and know what Arc validates at startup and what Chronicle enforces on append. +--- + +A string such as `@eventSourceType('Account')` names where an event goes, but nothing checks that the name exists or that the stream belongs to it. A Chronicle event source definition is that check: a registered class that declares a source and its streams. A command selects one with `@eventSourceDefinition`, and each event it returns records the definition it was appended through. + +Source and stream are routing. You declare them on the command or the aggregate, never on an event type. + +This feature needs `@cratis/chronicle` 6.49.0 or later. Older SDKs keep working for string routing, and a command that selects a definition fails with a message naming the required version. + +## Declare a definition and select it + +Declare the definition with the SDK's decorators, then select it on the command. + +```typescript +import { field } from '@cratis/fundamentals'; +import { ConcurrencyDimensions, eventSource, eventStream } from '@cratis/chronicle'; +import { command, key } from '@cratis/arc.core'; +import { eventSourceDefinition } from '@cratis/arc.chronicle'; + +@eventSource() +@eventStream('Transactions', { concurrency: ConcurrencyDimensions.eventStreamType | ConcurrencyDimensions.eventStreamId }) +export class Account {} + +@eventSourceDefinition(Account, 'Transactions') +@command() +export class Deposit { + @field(String) @key() id = ''; + handle(): FundsDeposited { return new FundsDeposited(); } +} +``` + +`FundsDeposited` is an `@eventType()` class. The event is appended with source `Account` and stream type `Transactions`. Chronicle records the definition on the event, and a reactor or projection reads it from `EventContext.eventSource`. + +The first argument is the class, its registered name, or a function returning the class. Use the function form, `@eventSourceDefinition(() => Account, 'Transactions')`, when the definition's module imports the command and the plain class would be read before it is defined. + +Referencing the class registers it with Chronicle, so you do not add it to discovery separately. A name can only be resolved against definitions that are registered, so a name Arc cannot find fails at startup. + +## What fails at startup + +Arc checks every command that selects a definition when the application is built, without a connection, and refuses to build when: + +- the class is not decorated with `@eventSource()`; +- the stream is not one the definition declares; +- a name matches no registered definition; or +- `@eventSourceType` or `@eventStreamType` on the same command contradicts the definition. Repeating the definition's own name is allowed. + +An aggregate, which Arc only meets when a command uses it, is checked the first time it loads. + +## Concurrency comes from the definition + +When a command selects a definition and sets no concurrency flags, Arc passes no scope and Chronicle derives one from the dimensions the definition or stream declares. Explicit flags win: `@eventStreamId('2026-05', { concurrency: true })` builds the same explicit scope as without a definition, using the definition's source name, and Chronicle then derives nothing. + +Chronicle's client refuses events for one event source ID that would need different automatic scopes within a single batch. Arc does not work around that; the command fails and appends nothing. Pass an explicit scope with `eventsWithConcurrencyScopes`, or return the events in separate commands. + +## Override one event + +An event entry that names its own `eventSource` and `eventStream`, or its own raw `eventSourceType` or `eventStreamType`, takes over source and stream for that event as one unit. The command's definition is neither merged into it nor used to rewrite it. + +```typescript +import { eventForEventSourceId } from '@cratis/arc.chronicle'; + +handle() { + return eventForEventSourceId({ eventSourceId: this.id, event: new FundsPosted(), + eventSource: Ledger, eventStream: 'Postings' }); +} +``` + +A raw `eventStreamType` on an entry wins over the command's definition the same way, and the entry is appended without a definition. The stream ID stays a separate default from `getEventStreamId()`. + +## Aggregates + +Declare the definition on the aggregate class to guard and rehydrate only that source and stream. + +```typescript +@eventSourceDefinition(Account, 'Transactions') +export class Wallet extends AggregateRoot { + constructor() { super(); this.on(FundsDeposited, () => {}); } +} +``` + +Loading reads the tail and the events for the declared source and stream only, the concurrency scope carries the same source and stream, and every event the aggregate applies records the definition. A command and its aggregate may each declare a definition when they agree; two that name different sources or streams fail instead of one silently winning. + +## Related + +- [Event metadata](event-metadata.md) +- [Concurrency](concurrency.md) +- [Defining an aggregate root](../aggregates/defining-an-aggregate-root.md) diff --git a/Documentation/chronicle/commands/toc.yml b/Documentation/chronicle/commands/toc.yml index 30c7e332..659b47d9 100644 --- a/Documentation/chronicle/commands/toc.yml +++ b/Documentation/chronicle/commands/toc.yml @@ -2,6 +2,8 @@ href: index.md - name: Event metadata href: event-metadata.md +- name: Event source definitions + href: event-source-definitions.md - name: Subject href: subject.md - name: Concurrency diff --git a/Documentation/decorators.md b/Documentation/decorators.md index 6c5099a4..05e27dca 100644 --- a/Documentation/decorators.md +++ b/Documentation/decorators.md @@ -87,6 +87,7 @@ From `@cratis/arc.chronicle` (experimental): | `@eventSourceType('Type', { concurrency? })` | command class | Default event source type for returned events | Command event metadata | | `@eventStreamType('Type', { concurrency? })` | command class | Default event stream type | Command event metadata | | `@eventStreamId('id', { concurrency? })` | command class | Default event stream ID | Command event metadata | +| `@eventSourceDefinition(Source, 'Stream'?)` | command or aggregate class | Route events through a Chronicle event source definition and stream | [Event source definitions](chronicle/commands/event-source-definitions.md) | | `@eventSubject('subject')` | command class | Default compliance subject | Command event metadata | | `@notAudited()` | command field | Keeps the value out of the causation chain | `[NotAudited]` | diff --git a/Documentation/reference/capabilities.md b/Documentation/reference/capabilities.md index e79033cd..92475983 100644 --- a/Documentation/reference/capabilities.md +++ b/Documentation/reference/capabilities.md @@ -123,10 +123,11 @@ Evidence paths are relative to the repository root. Spec folders follow `for_