Skip to content
Merged
2 changes: 1 addition & 1 deletion Documentation/chronicle/add-event-sourcing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.19.0 rxjs@^7.8.2
npm install ../arc-packages/arc.chronicle.tgz @cratis/chronicle@~6.29.0 rxjs@^7.8.2
```

| Package | What it gives you |
Expand Down
2 changes: 1 addition & 1 deletion Documentation/chronicle/reactors/scoped-activation.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ builder.withChronicle({
});
```

Scoped activation requires `@cratis/chronicle` 6.17.0 or later, the lowest version `@cratis/arc.chronicle` accepts as a peer. To use a Chronicle client you create yourself, see [Use a caller-owned client](#use-a-caller-owned-client).
Scoped activation requires `@cratis/chronicle` 6.29.0 or later, the lowest version `@cratis/arc.chronicle` accepts as a peer. To use a Chronicle client you create yourself, see [Use a caller-owned client](#use-a-caller-owned-client).

With the option set, Chronicle-only artifacts that `discover(...)` found before `withChronicle` was called are registered with Chronicle too. Without it, registration is unchanged.

Expand Down
4 changes: 4 additions & 0 deletions Documentation/chronicle/read-models/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ Arc's `@readModel()` exposes the queries. Chronicle infers the same class as its

`observeAll` keys the list by each model's `id`. Pass a key selector when your model names its identity differently, or when `id` is a concept, as `allAuthors` does with `author.id.toString()`. Unsubscribe, or let Arc end the subscription, to stop watching. SDK 6.9.1 and later omit the empty subscription marker from `watch()`; Arc also filters empty keys for older SDKs in its peer range.

## Where the read model is stored

Chronicle stores the projected read model in a container, a MongoDB collection by default. When the application also uses `withMongoDB` and Arc creates the Chronicle client, a class listed in its `readModels` is stored in the collection Arc's MongoDB integration reads, so `Author` is stored in `Authors` under the default naming policy. Any other read model, and every read model without `withMongoDB`, is stored under its identifier. `ChronicleReadModels` asks the kernel for the read model, so your queries never spell the name. See [Choose where read models are stored](../registration-options.md#choose-where-read-models-are-stored) for overrides and for a client you create yourself.

## Consistency

- **Active projections are eventually consistent by default.** A command can succeed before its read model has updated. A client that reads right after a command can see the old state. An observable query catches up on its own. For a passive on-demand read or a bounded observer wait after a command, see [Read consistency](../../queries/read-consistency.md).
Expand Down
14 changes: 13 additions & 1 deletion Documentation/chronicle/registration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Each recorded reactor and reducer also gets a scoped service registration in Arc
| `connectionString` | `string` | One of `connectionString` and `client` | Arc creates, connects, and disposes the SDK client |
| `client` | `IChronicleClient` from `@cratis/chronicle` | One of `connectionString` and `client` | You own the client; see [Choose who owns the client](#choose-who-owns-the-client) |
| `completionTimeoutMs` | positive integer, milliseconds | No; no wait by default | After each successful append, wait until Chronicle's observers have processed it before the command answers. See [Choose Chronicle read consistency](../queries/read-consistency.md) |
| `readModelNamingPolicy` | `(identifier, readModelType?) => string` | No | Only with `connectionString`. Names the container each read model is stored in. Defaults to Arc's MongoDB collection name when `withMongoDB` is configured, otherwise to the read model identifier. See [Choose where read models are stored](#choose-where-read-models-are-stored) |
| `activateArtifactsInScopes` | `boolean` | No; off by default | Preview. Resolve reactors and reducers from Arc's container, one scope per delivery. With `client`, registration verifies only that the client was created with `chronicleArtifactActivator`; passing `reactorCommandResultHandler` as `reactorResultHandler` is up to you. See [Scoped activation](reactors/scoped-activation.md) |

Registration throws `Chronicle requires eventStore and exactly one of connectionString or client` when the event store is missing, or when neither or both of a connection string and a client are set.
Expand All @@ -52,7 +53,7 @@ Every append and read uses the current execution's tenant as the Chronicle names
}
```

With that file, call `builder.withChronicle({})`. Keys are case-insensitive. To override the file in a deployment, set `Cratis__Chronicle__ConnectionString` and `Cratis__Chronicle__EventStore`. Only these two keys are read from configuration; `client` and `completionTimeoutMs` are set in code.
With that file, call `builder.withChronicle({})`. Keys are case-insensitive. To override the file in a deployment, set `Cratis__Chronicle__ConnectionString` and `Cratis__Chronicle__EventStore`. Only these two keys are read from configuration; `client`, `completionTimeoutMs`, and `readModelNamingPolicy` are set in code.

Values follow this precedence:

Expand All @@ -69,6 +70,17 @@ Values follow this precedence:

An Arc-owned client is also wired so that [reactors can return Arc commands](reactors/command-side-effects.md). A caller-owned client needs that handler passed to the SDK before it connects; the reactor page shows how. To use [scoped activation](reactors/scoped-activation.md#use-a-caller-owned-client) with a caller-owned client, also pass `chronicleArtifactActivator` as its `artifactActivator`. Dispose the Arc application before the client, so deliveries still running finish while the connection is open.

## Choose where read models are stored

A projected read model is stored in a container, which is a MongoDB collection by default. The Chronicle SDK names it after the read model identifier unless it is given a `readModelNamingPolicy`, a function of the identifier and, when the SDK knows it, the read model class. It changes only the container name.

- **With `withMongoDB`.** An Arc-owned client gets a policy that returns the collection Arc's MongoDB integration reads for a class listed in `readModels`, so a projected read model lands where your queries look with no configuration. A class outside that list, and a read model known only by identifier, keep the identifier. This holds whichever of `withMongoDB` and `withChronicle` you call first. See [Naming policies](../mongodb/naming-policies.md#chronicle-projected-read-models) for the rule and for upgrading.
- **With your own `readModelNamingPolicy`.** It replaces the automatic policy. Return the identifier when `readModelType` is undefined.
- **Without `withMongoDB`.** Arc adds no policy, and the SDK default, the identifier, applies.
- **With a caller-owned `client`.** Arc never changes the client, so it sets no policy, and `withChronicle` throws if you also pass `readModelNamingPolicy`. Set the policy in the `ChronicleOptions` you create the client with.

The option needs `@cratis/chronicle` 6.29.0 or later.

## Related

- [Add event sourcing](add-event-sourcing.md)
Expand Down
11 changes: 5 additions & 6 deletions Documentation/client-snippets/scenarios/camel-casing/setup.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
```typescript
import { field, Guid } from '@cratis/fundamentals';
import { ArcApplication, key } from '@cratis/arc.core';
import '@cratis/arc.chronicle';
import { camelCaseMongoNamingPolicy } from '@cratis/arc.mongodb';

// Users/User.ts
Expand All @@ -16,13 +17,11 @@ builder.withMongoDB({
server: 'mongodb://localhost:27017',
database: 'my-app',
readModels: [User],
namingPolicy: camelCaseMongoNamingPolicy,
// Chronicle's TypeScript client stores a projected read model in a collection named after its
// identifier: the class name, unpluralized (User), unless @readModel gives it another id. Both
// built-in policies pluralize (users), so read the collection Chronicle writes. Return the
// @readModel id instead for a read model that sets one.
collectionName: type => type.name
namingPolicy: camelCaseMongoNamingPolicy
});
// The Chronicle client Arc creates stores a projected User in the collection this policy reads (users),
// because withMongoDB is configured. No collectionName override is needed.
builder.withChronicle({ connectionString: 'chronicle://localhost:35000', eventStore: 'my-app' });
const app = await builder.build();
await app.run();
```
21 changes: 21 additions & 0 deletions Documentation/mongodb/naming-policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ builder.withMongoDB({ client, database: 'library', readModels: [Person],

The function receives each registered model class and must return a nonempty name for all of them, or the request that resolves the collection fails with `MongoDB collection name is required`.

## Chronicle-projected read models

When Chronicle projects a read model into MongoDB, the collection name has to match the one Arc reads. If the Chronicle client is created by [`withChronicle`](../chronicle/registration-options.md) with a `connectionString` and the application also calls `withMongoDB`, Arc gives that client a naming policy that applies this page's rule to a read model class: `collectionName?.(model) ?? namingPolicy.collectionName(model)`. A projected `User` lands in `Users` under the default policy and in `users` under `camelCaseMongoNamingPolicy`, with no extra setup. The same collection name is used whether `withMongoDB` is called before or after `withChronicle`.

The rule covers only the classes listed in `readModels`, because those are the only classes Arc reads from MongoDB: `mongoCollection(...)` and command read-model resolution both work from that list. A read model class that is not in `readModels`, such as a Chronicle-only read model served through `ChronicleReadModels`, keeps Chronicle's default collection name, its identifier. So does a read model Chronicle knows only by identifier, such as a projection with a custom `.containerName(...)`. The policy changes only the collection name. The database must still match: Chronicle writes the default namespace's read models to a database named after the event store, and to `<event store>+<namespace>` for any other namespace. `withMongoDB` uses `<database>` and `<database>+<tenantId>`, so set `database` to the event store name.

Two cases are not wired automatically:

- **You pass your own Chronicle `client`.** Arc never changes a client it does not own. Create it with `ChronicleOptions.fromConnectionString(connectionString, { readModelNamingPolicy })` and return the name you want, for example `(identifier, readModelType) => readModelType ? resolveMongoCollectionName(mongoOptions, readModelType) : identifier`. `resolveMongoCollectionName`, exported from `@cratis/arc.mongodb`, is the function Arc itself uses, and `mongoOptions` is an object with the same `namingPolicy` and `collectionName` you pass to `withMongoDB`. Passing `readModelNamingPolicy` to `withChronicle` together with a `client` throws.
- **You pass `readModelNamingPolicy` to `withChronicle`.** Your policy replaces the automatic one.

Without `withMongoDB`, Arc sets no policy and Chronicle names the collection after the read model identifier. The automatic policy needs `@cratis/chronicle` 6.29.0 or later, which is the lowest version `@cratis/arc.chronicle` accepts.

:::caution[Upgrading]
Before this behavior, a projected read model was stored in a collection named after its identifier, such as `User`, which the default `withMongoDB` policy never read. A workaround was `collectionName: model => model.name`. That workaround still gives `User`, so an application that uses it keeps its collection. An application on the default policy now has Chronicle register `Users` instead.

Replay the projection after upgrading so the new collection is populated from the event log. The previous collection is left as it is, so drop it yourself when you no longer need it. This is guidance from reading the Chronicle kernel's registration code (`ReadModelsManager.Register` and the projection definition comparer on `main` of Cratis/Chronicle, release 19.22.1): a registered read model definition, including its container name, replaces the stored one, the kernel writes and reads through the new name, and it moves no data. Confirm it against your own kernel version before relying on it.

A `collectionName` override that throws, or returns an empty name, for a class in `readModels` now fails when the Arc-created Chronicle client registers its read models, not only when a request resolves the collection.
:::

## Write a custom policy

A `MongoNamingPolicy` is two functions:
Expand Down
4 changes: 2 additions & 2 deletions Documentation/reference/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Evidence paths are relative to the repository root. Spec folders follow `for_<Su

- **MongoDB.** Also [joined observation](../mongodb/joined-observe.md), a [scoped watcher](../mongodb/change-stream-watcher.md), [GeoJSON geometry](../mongodb/geospatial.md), bounded transient read retries, MongoDB driver metrics for Arc-owned clients, and `Cratis:MongoDB:{Server,Database}` configuration binding. No durable watcher checkpoint; nonresumable stream failures terminate subscriptions. .NET's process-wide watcher, general-purpose resilience interceptors, and comprehensive metrics for supplied clients are not implemented.
- **SQL with Drizzle.** `bash Source/Drizzle/run-integration.sh` exercises live PostgreSQL and MySQL for existing SQL reads and command lookup, not observation. In-process observation is checked using SQLite (`sql.js`), gated race and burst specs, and SSE/GET through Express, Fastify, and Hono. Command read models load by a single column-level `.primaryKey()` with `@field` in the tenant scope; models without `@field` on their column-level key still serve queries but cannot be injected into commands. Tables without a column-level primary key are rejected at registration. Custom columns bind typed keys, plain columns primitives. Command read models are tested against SQLite, live MySQL 8.4, and live PostgreSQL 16 with node-postgres. PostgreSQL coverage includes typed GUID and concept keys, tenant isolation, and missing required and optional rows.
- **Chronicle.** It also resolves Chronicle read models by command key and in validators, batches nested returned events, and executes Arc commands returned from reactors through the SDK reactor result hook. SDK 6.19.0 loads in native Node ESM. Keyed aggregates and returned reactor commands are experimental.
- **Chronicle.** It also resolves Chronicle read models by command key and in validators, batches nested returned events, and executes Arc commands returned from reactors through the SDK reactor result hook. SDK 6.29.0 loads in native Node ESM. Keyed aggregates and returned reactor commands are experimental.
- **Chronicle compliance.** Mark projected read-model properties `@pii()` to encrypt them at rest; event-only marking does not protect the materialized field. Chronicle kernel reads already release values (including command injection), and Arc skips releasing those instances twice. For protected Chronicle models decoded into the exact read-model class by `MongoCollection`, Arc releases at the query edge, including snapshots, pages, and observable emissions. Raw `MongoReadModels` documents typed with `readModel` are released with the request's tenant and each document's subject: `subjectFor(document)` when given, otherwise the stored `__subject`, otherwise a string or numeric `_id`. It must match any stored `__subject` and the model's `@subject()` or `id`. Kernel bookkeeping fields are stripped. The path fails closed on undeclared fields, non-JSON BSON values (including `Guid` fields stored as `Binary`), per-property `__subjects`, a subject or tenant mismatch, typed documents nested in another shape, and MongoDB projections. Unreleased instances of a protected class nested in another returned shape, such as a joined `select` result, fail the query. Specs check snapshots, pages, and observable SSE through Express, Fastify, and Hono against a Chronicle substitute; the kernel integration reads a real materialized document through `MongoReadModels`. Untyped raw documents, typed documents of models not registered with Chronicle, codec-selected derived subtypes, `MongoDBWatcher.changes()` payloads, documents hidden behind `toJSON()`, getters, or private fields, and DTOs, mapped objects, or copies are served as stored and need explicit `readModels.release` on the tenant store. Nested, array-item, class-level PII and `@encrypted()` security metadata are detected. A directly read protected model needs `@subject()` or `id` matching the event subject to release; one without either is served only when it holds no protected value, and fails otherwise. The kernel integration checks raw MongoDB ciphertext, Chronicle delivery, and Arc query-edge release for a direct MongoDB read.

## Testing
Expand Down Expand Up @@ -184,7 +184,7 @@ The integration pages describe behavior. This section records the checks behind

### Chronicle checks

The specs run with the published Chronicle TypeScript SDK, `@cratis/chronicle` 6.19.0, and `@cratis/fundamentals` 7.19.6; both load in native Node ESM with NodeNext resolution. The live suite was last verified with SDK 6.10.0; a 6.14.0 attempt could not start the local kernel because its MongoDB connection was refused. The ordinary `yarn test` specs use typed substitutes or the SDK's in-memory `ReadModelScenario` and never start a kernel. An opt-in suite, `bash Source/Chronicle/run-integration.sh`, runs [`Source/Chronicle/Integration/live.test.mjs`](https://github.com/Cratis/Arc.TypeScript/blob/main/Source/Chronicle/Integration/live.test.mjs) against a real development kernel and checks:
The specs run with the published Chronicle TypeScript SDK, `@cratis/chronicle` 6.29.0, and `@cratis/fundamentals` 7.19.6; both load in native Node ESM with NodeNext resolution. The live suite was last verified with SDK 6.10.0; a 6.14.0 attempt could not start the local kernel because its MongoDB connection was refused. The ordinary `yarn test` specs use typed substitutes or the SDK's in-memory `ReadModelScenario` and never start a kernel. An opt-in suite, `bash Source/Chronicle/run-integration.sh`, runs [`Source/Chronicle/Integration/live.test.mjs`](https://github.com/Cratis/Arc.TypeScript/blob/main/Source/Chronicle/Integration/live.test.mjs) against a real development kernel and checks:

- returned-event batches, readback, and tenant isolation;
- a reactor that returns an Arc command, executed in the triggering event's tenant;
Expand Down
Loading
Loading