diff --git a/Documentation/chronicle/add-event-sourcing.md b/Documentation/chronicle/add-event-sourcing.md index 1f4f5899..197497ff 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.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 | diff --git a/Documentation/chronicle/reactors/scoped-activation.md b/Documentation/chronicle/reactors/scoped-activation.md index 2e1bccfe..b8c06ee6 100644 --- a/Documentation/chronicle/reactors/scoped-activation.md +++ b/Documentation/chronicle/reactors/scoped-activation.md @@ -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. diff --git a/Documentation/chronicle/read-models/index.md b/Documentation/chronicle/read-models/index.md index dc7f8333..38df0539 100644 --- a/Documentation/chronicle/read-models/index.md +++ b/Documentation/chronicle/read-models/index.md @@ -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). diff --git a/Documentation/chronicle/registration-options.md b/Documentation/chronicle/registration-options.md index 28de0917..95113952 100644 --- a/Documentation/chronicle/registration-options.md +++ b/Documentation/chronicle/registration-options.md @@ -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. @@ -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: @@ -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) diff --git a/Documentation/client-snippets/scenarios/camel-casing/setup.md b/Documentation/client-snippets/scenarios/camel-casing/setup.md index b5511dbe..a8a86397 100644 --- a/Documentation/client-snippets/scenarios/camel-casing/setup.md +++ b/Documentation/client-snippets/scenarios/camel-casing/setup.md @@ -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 @@ -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(); ``` diff --git a/Documentation/mongodb/naming-policies.md b/Documentation/mongodb/naming-policies.md index 0f25356a..cb715365 100644 --- a/Documentation/mongodb/naming-policies.md +++ b/Documentation/mongodb/naming-policies.md @@ -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 `+` for any other namespace. `withMongoDB` uses `` and `+`, 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: diff --git a/Documentation/reference/capabilities.md b/Documentation/reference/capabilities.md index b4907a3c..53edfab7 100644 --- a/Documentation/reference/capabilities.md +++ b/Documentation/reference/capabilities.md @@ -126,7 +126,7 @@ Evidence paths are relative to the repository root. Spec folders follow `for_