From 48490f556c897846fd717570c9186bd46c830da0 Mon Sep 17 00:00:00 2001 From: woksin Date: Sun, 5 Jul 2026 08:09:02 +0200 Subject: [PATCH] Widen Chronicle client-docs tabs to real TypeScript parity Chronicle's shared docs previously fell back to C#-only tabs in many places because TypeScript coverage was never written, not because the SDK lacked the capability. Add real, source-verified TypeScript snippets everywhere an equivalent API exists (events, reactors, reducers, projections, constraints, compliance, subscriptions, seeding, materialized pagination, event migrations, dynamic tags, correlation/identity/causation, and more), and add explicit "not supported" stubs for the confirmed genuine gaps (including several fluent-builder methods that type-check but throw 'not implemented yet' at runtime) so every tab is either real or honestly scoped, never silently missing. Also trims TypeScript client-specific pages that duplicated shared concept explanations (correlation, identity, auditing, sinks, jobs, webhooks) down to cross-links into the shared docs, and adds a shared Jobs/Webhooks page (previously undocumented anywhere but Elixir and TypeScript's own client pages, at inconsistent depth). --- .github/workflows/client-snippets.yml | 58 +++ .github/workflows/documentation.yml | 27 + .../validate-client-snippets.cpython-314.pyc | Bin 0 -> 12779 bytes Documentation/auditing.md | 2 +- .../coming-from-crud/events.md | 13 + .../coming-from-crud/read-model.md | 15 + .../coming-from-crud/write-and-read.md | 12 + .../compliance/client/concept-annotation.md | 11 + .../compliance/client/property-annotation.md | 11 + .../pii-with-concepts/defining-concept.md | 11 + .../pii-with-concepts/national-id.md | 11 + .../pii-with-concepts/with-details.md | 11 + .../compliance/pii/concept-level.md | 11 + .../compliance/pii/legal-name-details.md | 11 + .../compliance/pii/property-level.md | 10 + .../compliance/pii/with-details.md | 11 + .../causation.md | 9 + .../correlation.md | 13 + .../identity.md | 13 + .../designing-read-models/query-ladder.md | 24 + .../strongly-consistent.md | 18 + .../concepts/event-type-migrations/combine.md | 30 ++ .../event-type-migrations/default-value.md | 30 ++ .../defining-migrations.md | 30 ++ .../concepts/event-type-migrations/rename.md | 26 + .../concepts/event-type-migrations/split.md | 30 ++ .../modeling-events/fact-vs-intent.md | 22 + .../concepts/modeling-events/no-nullable.md | 41 ++ .../modeling-events/single-purpose.md | 49 ++ .../concepts/subject/default.md | 3 + .../concepts/subject/explicit.md | 3 + .../concepts/tagging/dynamic-event-tags.md | 3 + .../constraints/declarative/index/defining.md | 22 + .../declarative/unique-event-type/basic.md | 14 + .../unique-event-type/constraint-name.md | 17 + .../unique-event-type/violation-message.md | 14 + .../constraints/declarative/unique/basic.md | 22 + .../declarative/unique/constraint-name.md | 18 + .../declarative/unique/ignore-casing.md | 18 + .../unique/multiple-event-types.md | 29 ++ .../declarative/unique/releasing.md | 22 + .../declarative/unique/violation-message.md | 18 + .../model-bound/unique-event-type/basic.md | 3 + .../unique-event-type/name-and-message.md | 3 + .../unique-event-type/releasing.md | 3 + .../constraints/model-bound/unique/basic.md | 3 + .../model-bound/unique/grouping.md | 3 + .../unique/multiple-remove-constraints.md | 3 + .../model-bound/unique/releasing.md | 3 + .../event-stores-definition.md | 8 + .../namespaces-definition.md | 7 + .../request-messages.md | 9 + .../typescript-grpc-package/service-types.md | 10 + .../events/appending-many/transfer.md | 30 ++ .../events/appending-with-tags/append-many.md | 4 + .../events/appending-with-tags/append.md | 4 + .../events/appending/example.md | 26 + .../events/appending/schema-validation.md | 12 + .../events/concurrency/basic.md | 21 + .../events/getting-events/for-event-source.md | 3 + .../events/getting-state/tail.md | 12 + .../client-snippets/events/index/event-log.md | 11 + .../events/index/event-type.md | 11 + .../events/transactions/basic.md | 34 ++ .../get-started/client-flow.md | 9 + .../get-started/common/a-events.md | 20 + .../get-started/common/append.md | 13 + .../get-started/common/book-read-model.md | 23 + .../common/borrowed-book-read-model.md | 13 + .../get-started/common/materialized-paging.md | 11 + .../get-started/common/query-read-models.md | 14 + .../get-started/common/reactor.md | 13 + .../get-started/console/connect.md | 12 + .../client-snippets/get-started/test-event.md | 8 + .../get-started/test-projection.md | 9 + .../get-started/test-reactor.md | 10 + .../client-snippets/jobs/index/get-one.md | 12 + .../client-snippets/jobs/index/get-steps.md | 12 + .../client-snippets/jobs/index/list-all.md | 12 + .../jobs/index/stop-resume-delete.md | 19 + .../projections/architecture/declarative.md | 6 + .../projections/architecture/model-bound.md | 17 + .../choosing-a-read-model-style/events.md | 18 + .../choosing-a-read-model-style/fluent.md | 30 ++ .../model-bound.md | 22 + .../choosing-a-read-model-style/read-model.md | 9 + .../choosing-a-read-model-style/reducer.md | 43 ++ .../projections/declarative/auto-map/basic.md | 28 + .../declarative/auto-map/disable.md | 27 + .../declarative/auto-map/explicit-mappings.md | 32 ++ .../projections/declarative/auto-map/join.md | 30 ++ .../declarative/composite-keys/audit-model.md | 3 + .../declarative/composite-keys/basic.md | 3 + .../composite-keys/child-collection.md | 3 + .../declarative/composite-keys/context-key.md | 3 + .../declarative/composite-keys/events.md | 3 + .../declarative/composite-keys/join.md | 3 + .../declarative/composite-keys/key-type.md | 3 + .../declarative/composite-keys/read-model.md | 3 + .../constant-key/increment-decrement.md | 28 + .../event-context/automap-with-context.md | 21 + .../declarative/event-context/basic.md | 16 + .../declarative/event-context/events.md | 14 + .../declarative/event-context/read-models.md | 15 + .../projections/declarative/from-all/basic.md | 33 ++ .../declarative/from-all/context-fields.md | 28 + .../declarative/from-all/event-source-id.md | 26 + .../from-all/with-specific-mappings.md | 37 ++ .../declarative/from-event-sequence/basic.md | 15 + .../declarative/from-event-sequence/events.md | 22 + .../from-event-sequence/multiple-sequences.md | 51 ++ .../from-event-sequence/read-model.md | 18 + .../from-event-sequence/using-constant.md | 19 + .../declarative/from-every/basic.md | 32 ++ .../declarative/from-every/context-fields.md | 27 + .../declarative/from-every/event-source-id.md | 25 + .../from-every/exclude-children.md | 26 + .../from-every/multiple-declarations.md | 29 ++ .../functions/increment-decrement.md | 29 ++ .../projections/declarative/index/basic.md | 23 + .../declarative/initial-values/basic.md | 35 ++ .../initial-values/business-defaults.md | 33 ++ .../declarative/initial-values/collections.md | 29 ++ .../initial-values/partial-events.md | 3 + .../projections/declarative/joins/basic.md | 42 ++ .../declarative/joins/multiple-joins.md | 46 ++ .../declarative/joins/read-model.md | 9 + .../not-rewindable/audit-use-case.md | 34 ++ .../declarative/not-rewindable/basic.md | 16 + .../not-rewindable/combined-features.md | 42 ++ .../declarative/not-rewindable/events.md | 17 + .../not-rewindable/financial-use-case.md | 27 + .../not-rewindable/performance-use-case.md | 25 + .../declarative/not-rewindable/read-model.md | 10 + .../projections/declarative/passive/events.md | 20 + .../declarative/passive/projection.md | 14 + .../declarative/passive/read-model.md | 8 + .../declarative/passive/service.md | 12 + .../set-properties/combined-automap.md | 15 + .../declarative/set-properties/events.md | 21 + .../declarative/set-properties/explicit.md | 19 + .../declarative/set-properties/read-model.md | 10 + .../declarative/simple-projection/event.md | 7 + .../simple-projection/projection.md | 10 + .../simple-projection/read-model.md | 7 + .../projections/eventual-consistency/cqs.md | 31 ++ .../eventual-consistency/design-for-async.md | 34 ++ .../projections/eventual-consistency/watch.md | 40 ++ .../filtering/appending-with-metadata.md | 7 + .../projections/filtering/basic.md | 23 + .../model-bound/basic-mapping/add-from.md | 22 + .../model-bound/basic-mapping/complete.md | 39 ++ .../basic-mapping/context-vs-from-every.md | 27 + .../basic-mapping/set-from-context.md | 18 + .../basic-mapping/set-from-convention.md | 18 + .../basic-mapping/set-from-multiple-events.md | 22 + .../model-bound/basic-mapping/set-from.md | 18 + .../basic-mapping/subtract-from.md | 29 ++ .../model-bound/children/automap.md | 26 + .../projections/model-bound/children/basic.md | 27 + .../model-bound/children/child-from-event.md | 32 ++ .../model-bound/children/full-example.md | 55 ++ .../model-bound/children/nested-hierarchy.md | 65 +++ .../children/removal-class-level.md | 29 ++ .../children/removal-property-level.md | 28 + .../model-bound/children/removed-with-join.md | 28 + .../model-bound/children/with-counters.md | 45 ++ .../model-bound/constant-key/basic.md | 19 + .../model-bound/constant-key/counters.md | 32 ++ .../model-bound/constant-key/full-example.md | 38 ++ .../model-bound/constant-key/mixed-keys.md | 22 + .../convention-based/automap-equivalent.md | 22 + .../model-bound/convention-based/basic.md | 20 + .../convention-based/custom-key.md | 19 + .../convention-based/explicit-equivalent.md | 25 + .../convention-based/matching-structures.md | 51 ++ .../convention-based/multiple-events.md | 26 + .../convention-based/partial-events.md | 27 + .../model-bound/counters/basic-increment.md | 15 + .../model-bound/counters/combined.md | 36 ++ .../projections/model-bound/counters/count.md | 22 + .../model-bound/counters/full-example.md | 54 ++ .../counters/increment-decrement.md | 20 + .../event-sequence-source/fluent.md | 21 + .../model-bound/from-every/combined.md | 35 ++ .../model-bound/from-every/context-fields.md | 21 + .../from-every/context-property.md | 23 + .../model-bound/from-every/convention.md | 24 + .../from-every/declarative-equivalent.md | 31 ++ .../model-bound/from-every/event-property.md | 27 + .../model-bound/index/automap-fluent.md | 21 + .../model-bound/index/automap-model-bound.md | 11 + .../projections/model-bound/index/basic.md | 19 + .../index/explicit-mapping-fluent.md | 23 + .../index/explicit-mapping-model-bound.md | 13 + .../projections/model-bound/joins/basic.md | 28 + .../projections/model-bound/joins/children.md | 38 ++ .../model-bound/joins/full-example.md | 79 +++ .../joins/multiple-join-sources.md | 41 ++ .../model-bound/joins/multiple-joins.md | 42 ++ .../model-bound/not-rewindable/basic.md | 21 + .../projections/model-bound/passive/basic.md | 17 + .../projections/model-bound/removal/basic.md | 25 + .../model-bound/removal/full-example.md | 55 ++ .../model-bound/removal/join-on-class.md | 29 ++ .../model-bound/removal/multiple-triggers.md | 32 ++ .../model-bound/removal/with-key.md | 22 + .../model-bound/set-value/invoice.md | 26 + .../model-bound/set-value/order.md | 26 + .../model-bound/set-value/subscription.md | 28 + .../model-bound/set-value/thing.md | 25 + .../nested-objects-design/declarative.md | 38 ++ .../nested-objects-design/model-bound.md | 34 ++ .../nested-objects-design/recursive.md | 54 ++ .../auto-map/event.md | 10 + .../auto-map/read-model.md | 8 + .../children/read-model.md | 12 + .../counters/read-model.md | 7 + .../expressions/read-model.md | 8 + .../nested/read-model.md | 11 + .../event-processing/event-context.md | 19 + .../event-sequence/reactor-attribute.md | 19 + .../reactors/getting-started/event.md | 11 + .../reactors/getting-started/reactor.md | 21 + .../reactors/getting-started/register.md | 9 + .../reactors/getting-started/stable-id.md | 12 + .../reactors/index/basic-example.md | 19 + .../getting-collection-instances/basic.md | 7 + .../event-count.md | 5 + .../getting-collection-instances/filtering.md | 7 + .../getting-single-instance/basic.md | 5 + .../read-models/getting-snapshots/analyze.md | 10 + .../read-models/getting-snapshots/basic.md | 5 + .../materialized-pagination/accessing-api.md | 19 + .../materialized-pagination/basic-usage.md | 11 + .../materialized-pagination/observing.md | 21 + .../materialized-pagination/pagination.md | 21 + .../read-models/watching-read-models/basic.md | 9 + .../watching-read-models/filtering.md | 9 + .../event-sequence/reducer-attribute.md | 26 + .../reducers/getting-started/read-model.md | 8 + .../getting-started/reducer-attribute.md | 11 + .../getting-started/reducer-implementation.md | 60 +++ .../getting-started/retrieving-state.md | 11 + .../reducers/index/basic-example.md | 39 ++ .../react-to-an-event/basic-reactor.md | 25 + .../react-to-an-event/explicit-append.md | 3 + .../react-to-an-event/reading-state.md | 3 + .../scenarios/real-time-query/get-one.md | 18 + .../explicit-subscriptions/basic.md | 18 + .../explicit-subscriptions/filtering.md | 23 + .../explicit-subscriptions/inbox-reactor.md | 22 + .../naming-convention.md | 28 + .../explicit-subscriptions/no-filter.md | 10 + .../explicit-subscriptions/unsubscribe.md | 9 + .../outbox-inbox/inbox-reactor.md | 20 + .../tutorial/first-event/append.md | 11 + .../tutorial/first-event/book-added.md | 11 + .../tutorial/first-event/book-id.md | 3 + .../tutorial/reacting/explicit-append.md | 3 + .../reacting/notification-sent-event.md | 7 + .../tutorial/reacting/once-only.md | 3 + .../tutorial/reacting/reading-state.md | 3 + .../tutorial/reacting/side-effect.md | 3 + .../tutorial/reacting/waitlist-notifier.md | 17 + .../tutorial/read-model/a-events.md | 12 + .../tutorial/read-model/book.md | 23 + .../tutorial/read-model/borrowed-book.md | 13 + .../dedicated-class.md | 31 ++ .../client-snippets/webhooks/index/query.md | 12 + .../webhooks/index/register.md | 28 + Documentation/compliance/index.md | 155 +----- Documentation/compliance/pii.md | 490 +----------------- Documentation/compliance/read-models.md | 456 +--------------- Documentation/correlation.md | 2 +- Documentation/event-log.md | 141 +---- Documentation/event-type-migrations.md | 77 +-- Documentation/event-types.md | 57 +- Documentation/getting-started.md | 8 + Documentation/identity.md | 2 +- Documentation/index.md | 46 +- Documentation/jobs.md | 2 +- Documentation/projections.md | 13 + Documentation/reactors.md | 52 +- Documentation/read-models.md | 44 +- Documentation/reducers.md | 58 +-- Documentation/seeding.md | 113 +++- Documentation/sinks.md | 4 +- Documentation/toc.yml | 20 + Documentation/transactions.md | 55 +- Documentation/validate-client-snippets.py | 251 +++++++++ Documentation/webhooks.md | 2 +- 292 files changed, 5869 insertions(+), 1555 deletions(-) create mode 100644 .github/workflows/client-snippets.yml create mode 100644 .github/workflows/documentation.yml create mode 100644 Documentation/__pycache__/validate-client-snippets.cpython-314.pyc create mode 100644 Documentation/client-snippets/coming-from-crud/events.md create mode 100644 Documentation/client-snippets/coming-from-crud/read-model.md create mode 100644 Documentation/client-snippets/coming-from-crud/write-and-read.md create mode 100644 Documentation/client-snippets/compliance/client/concept-annotation.md create mode 100644 Documentation/client-snippets/compliance/client/property-annotation.md create mode 100644 Documentation/client-snippets/compliance/pii-with-concepts/defining-concept.md create mode 100644 Documentation/client-snippets/compliance/pii-with-concepts/national-id.md create mode 100644 Documentation/client-snippets/compliance/pii-with-concepts/with-details.md create mode 100644 Documentation/client-snippets/compliance/pii/concept-level.md create mode 100644 Documentation/client-snippets/compliance/pii/legal-name-details.md create mode 100644 Documentation/client-snippets/compliance/pii/property-level.md create mode 100644 Documentation/client-snippets/compliance/pii/with-details.md create mode 100644 Documentation/client-snippets/concepts/correlation-identity-causation/causation.md create mode 100644 Documentation/client-snippets/concepts/correlation-identity-causation/correlation.md create mode 100644 Documentation/client-snippets/concepts/correlation-identity-causation/identity.md create mode 100644 Documentation/client-snippets/concepts/designing-read-models/query-ladder.md create mode 100644 Documentation/client-snippets/concepts/designing-read-models/strongly-consistent.md create mode 100644 Documentation/client-snippets/concepts/event-type-migrations/combine.md create mode 100644 Documentation/client-snippets/concepts/event-type-migrations/default-value.md create mode 100644 Documentation/client-snippets/concepts/event-type-migrations/defining-migrations.md create mode 100644 Documentation/client-snippets/concepts/event-type-migrations/rename.md create mode 100644 Documentation/client-snippets/concepts/event-type-migrations/split.md create mode 100644 Documentation/client-snippets/concepts/modeling-events/fact-vs-intent.md create mode 100644 Documentation/client-snippets/concepts/modeling-events/no-nullable.md create mode 100644 Documentation/client-snippets/concepts/modeling-events/single-purpose.md create mode 100644 Documentation/client-snippets/concepts/subject/default.md create mode 100644 Documentation/client-snippets/concepts/subject/explicit.md create mode 100644 Documentation/client-snippets/concepts/tagging/dynamic-event-tags.md create mode 100644 Documentation/client-snippets/constraints/declarative/index/defining.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique-event-type/basic.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique-event-type/constraint-name.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique-event-type/violation-message.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique/basic.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique/constraint-name.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique/ignore-casing.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique/multiple-event-types.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique/releasing.md create mode 100644 Documentation/client-snippets/constraints/declarative/unique/violation-message.md create mode 100644 Documentation/client-snippets/constraints/model-bound/unique-event-type/basic.md create mode 100644 Documentation/client-snippets/constraints/model-bound/unique-event-type/name-and-message.md create mode 100644 Documentation/client-snippets/constraints/model-bound/unique-event-type/releasing.md create mode 100644 Documentation/client-snippets/constraints/model-bound/unique/basic.md create mode 100644 Documentation/client-snippets/constraints/model-bound/unique/grouping.md create mode 100644 Documentation/client-snippets/constraints/model-bound/unique/multiple-remove-constraints.md create mode 100644 Documentation/client-snippets/constraints/model-bound/unique/releasing.md create mode 100644 Documentation/client-snippets/contributing/clients/typescript-grpc-package/event-stores-definition.md create mode 100644 Documentation/client-snippets/contributing/clients/typescript-grpc-package/namespaces-definition.md create mode 100644 Documentation/client-snippets/contributing/clients/typescript-grpc-package/request-messages.md create mode 100644 Documentation/client-snippets/contributing/clients/typescript-grpc-package/service-types.md create mode 100644 Documentation/client-snippets/events/appending-many/transfer.md create mode 100644 Documentation/client-snippets/events/appending-with-tags/append-many.md create mode 100644 Documentation/client-snippets/events/appending-with-tags/append.md create mode 100644 Documentation/client-snippets/events/appending/example.md create mode 100644 Documentation/client-snippets/events/appending/schema-validation.md create mode 100644 Documentation/client-snippets/events/concurrency/basic.md create mode 100644 Documentation/client-snippets/events/getting-events/for-event-source.md create mode 100644 Documentation/client-snippets/events/getting-state/tail.md create mode 100644 Documentation/client-snippets/events/index/event-log.md create mode 100644 Documentation/client-snippets/events/index/event-type.md create mode 100644 Documentation/client-snippets/events/transactions/basic.md create mode 100644 Documentation/client-snippets/get-started/client-flow.md create mode 100644 Documentation/client-snippets/get-started/common/a-events.md create mode 100644 Documentation/client-snippets/get-started/common/append.md create mode 100644 Documentation/client-snippets/get-started/common/book-read-model.md create mode 100644 Documentation/client-snippets/get-started/common/borrowed-book-read-model.md create mode 100644 Documentation/client-snippets/get-started/common/materialized-paging.md create mode 100644 Documentation/client-snippets/get-started/common/query-read-models.md create mode 100644 Documentation/client-snippets/get-started/common/reactor.md create mode 100644 Documentation/client-snippets/get-started/console/connect.md create mode 100644 Documentation/client-snippets/get-started/test-event.md create mode 100644 Documentation/client-snippets/get-started/test-projection.md create mode 100644 Documentation/client-snippets/get-started/test-reactor.md create mode 100644 Documentation/client-snippets/jobs/index/get-one.md create mode 100644 Documentation/client-snippets/jobs/index/get-steps.md create mode 100644 Documentation/client-snippets/jobs/index/list-all.md create mode 100644 Documentation/client-snippets/jobs/index/stop-resume-delete.md create mode 100644 Documentation/client-snippets/projections/architecture/declarative.md create mode 100644 Documentation/client-snippets/projections/architecture/model-bound.md create mode 100644 Documentation/client-snippets/projections/choosing-a-read-model-style/events.md create mode 100644 Documentation/client-snippets/projections/choosing-a-read-model-style/fluent.md create mode 100644 Documentation/client-snippets/projections/choosing-a-read-model-style/model-bound.md create mode 100644 Documentation/client-snippets/projections/choosing-a-read-model-style/read-model.md create mode 100644 Documentation/client-snippets/projections/choosing-a-read-model-style/reducer.md create mode 100644 Documentation/client-snippets/projections/declarative/auto-map/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/auto-map/disable.md create mode 100644 Documentation/client-snippets/projections/declarative/auto-map/explicit-mappings.md create mode 100644 Documentation/client-snippets/projections/declarative/auto-map/join.md create mode 100644 Documentation/client-snippets/projections/declarative/composite-keys/audit-model.md create mode 100644 Documentation/client-snippets/projections/declarative/composite-keys/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/composite-keys/child-collection.md create mode 100644 Documentation/client-snippets/projections/declarative/composite-keys/context-key.md create mode 100644 Documentation/client-snippets/projections/declarative/composite-keys/events.md create mode 100644 Documentation/client-snippets/projections/declarative/composite-keys/join.md create mode 100644 Documentation/client-snippets/projections/declarative/composite-keys/key-type.md create mode 100644 Documentation/client-snippets/projections/declarative/composite-keys/read-model.md create mode 100644 Documentation/client-snippets/projections/declarative/constant-key/increment-decrement.md create mode 100644 Documentation/client-snippets/projections/declarative/event-context/automap-with-context.md create mode 100644 Documentation/client-snippets/projections/declarative/event-context/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/event-context/events.md create mode 100644 Documentation/client-snippets/projections/declarative/event-context/read-models.md create mode 100644 Documentation/client-snippets/projections/declarative/from-all/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/from-all/context-fields.md create mode 100644 Documentation/client-snippets/projections/declarative/from-all/event-source-id.md create mode 100644 Documentation/client-snippets/projections/declarative/from-all/with-specific-mappings.md create mode 100644 Documentation/client-snippets/projections/declarative/from-event-sequence/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/from-event-sequence/events.md create mode 100644 Documentation/client-snippets/projections/declarative/from-event-sequence/multiple-sequences.md create mode 100644 Documentation/client-snippets/projections/declarative/from-event-sequence/read-model.md create mode 100644 Documentation/client-snippets/projections/declarative/from-event-sequence/using-constant.md create mode 100644 Documentation/client-snippets/projections/declarative/from-every/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/from-every/context-fields.md create mode 100644 Documentation/client-snippets/projections/declarative/from-every/event-source-id.md create mode 100644 Documentation/client-snippets/projections/declarative/from-every/exclude-children.md create mode 100644 Documentation/client-snippets/projections/declarative/from-every/multiple-declarations.md create mode 100644 Documentation/client-snippets/projections/declarative/functions/increment-decrement.md create mode 100644 Documentation/client-snippets/projections/declarative/index/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/initial-values/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/initial-values/business-defaults.md create mode 100644 Documentation/client-snippets/projections/declarative/initial-values/collections.md create mode 100644 Documentation/client-snippets/projections/declarative/initial-values/partial-events.md create mode 100644 Documentation/client-snippets/projections/declarative/joins/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/joins/multiple-joins.md create mode 100644 Documentation/client-snippets/projections/declarative/joins/read-model.md create mode 100644 Documentation/client-snippets/projections/declarative/not-rewindable/audit-use-case.md create mode 100644 Documentation/client-snippets/projections/declarative/not-rewindable/basic.md create mode 100644 Documentation/client-snippets/projections/declarative/not-rewindable/combined-features.md create mode 100644 Documentation/client-snippets/projections/declarative/not-rewindable/events.md create mode 100644 Documentation/client-snippets/projections/declarative/not-rewindable/financial-use-case.md create mode 100644 Documentation/client-snippets/projections/declarative/not-rewindable/performance-use-case.md create mode 100644 Documentation/client-snippets/projections/declarative/not-rewindable/read-model.md create mode 100644 Documentation/client-snippets/projections/declarative/passive/events.md create mode 100644 Documentation/client-snippets/projections/declarative/passive/projection.md create mode 100644 Documentation/client-snippets/projections/declarative/passive/read-model.md create mode 100644 Documentation/client-snippets/projections/declarative/passive/service.md create mode 100644 Documentation/client-snippets/projections/declarative/set-properties/combined-automap.md create mode 100644 Documentation/client-snippets/projections/declarative/set-properties/events.md create mode 100644 Documentation/client-snippets/projections/declarative/set-properties/explicit.md create mode 100644 Documentation/client-snippets/projections/declarative/set-properties/read-model.md create mode 100644 Documentation/client-snippets/projections/declarative/simple-projection/event.md create mode 100644 Documentation/client-snippets/projections/declarative/simple-projection/projection.md create mode 100644 Documentation/client-snippets/projections/declarative/simple-projection/read-model.md create mode 100644 Documentation/client-snippets/projections/eventual-consistency/cqs.md create mode 100644 Documentation/client-snippets/projections/eventual-consistency/design-for-async.md create mode 100644 Documentation/client-snippets/projections/eventual-consistency/watch.md create mode 100644 Documentation/client-snippets/projections/filtering/appending-with-metadata.md create mode 100644 Documentation/client-snippets/projections/filtering/basic.md create mode 100644 Documentation/client-snippets/projections/model-bound/basic-mapping/add-from.md create mode 100644 Documentation/client-snippets/projections/model-bound/basic-mapping/complete.md create mode 100644 Documentation/client-snippets/projections/model-bound/basic-mapping/context-vs-from-every.md create mode 100644 Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-context.md create mode 100644 Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-convention.md create mode 100644 Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-multiple-events.md create mode 100644 Documentation/client-snippets/projections/model-bound/basic-mapping/set-from.md create mode 100644 Documentation/client-snippets/projections/model-bound/basic-mapping/subtract-from.md create mode 100644 Documentation/client-snippets/projections/model-bound/children/automap.md create mode 100644 Documentation/client-snippets/projections/model-bound/children/basic.md create mode 100644 Documentation/client-snippets/projections/model-bound/children/child-from-event.md create mode 100644 Documentation/client-snippets/projections/model-bound/children/full-example.md create mode 100644 Documentation/client-snippets/projections/model-bound/children/nested-hierarchy.md create mode 100644 Documentation/client-snippets/projections/model-bound/children/removal-class-level.md create mode 100644 Documentation/client-snippets/projections/model-bound/children/removal-property-level.md create mode 100644 Documentation/client-snippets/projections/model-bound/children/removed-with-join.md create mode 100644 Documentation/client-snippets/projections/model-bound/children/with-counters.md create mode 100644 Documentation/client-snippets/projections/model-bound/constant-key/basic.md create mode 100644 Documentation/client-snippets/projections/model-bound/constant-key/counters.md create mode 100644 Documentation/client-snippets/projections/model-bound/constant-key/full-example.md create mode 100644 Documentation/client-snippets/projections/model-bound/constant-key/mixed-keys.md create mode 100644 Documentation/client-snippets/projections/model-bound/convention-based/automap-equivalent.md create mode 100644 Documentation/client-snippets/projections/model-bound/convention-based/basic.md create mode 100644 Documentation/client-snippets/projections/model-bound/convention-based/custom-key.md create mode 100644 Documentation/client-snippets/projections/model-bound/convention-based/explicit-equivalent.md create mode 100644 Documentation/client-snippets/projections/model-bound/convention-based/matching-structures.md create mode 100644 Documentation/client-snippets/projections/model-bound/convention-based/multiple-events.md create mode 100644 Documentation/client-snippets/projections/model-bound/convention-based/partial-events.md create mode 100644 Documentation/client-snippets/projections/model-bound/counters/basic-increment.md create mode 100644 Documentation/client-snippets/projections/model-bound/counters/combined.md create mode 100644 Documentation/client-snippets/projections/model-bound/counters/count.md create mode 100644 Documentation/client-snippets/projections/model-bound/counters/full-example.md create mode 100644 Documentation/client-snippets/projections/model-bound/counters/increment-decrement.md create mode 100644 Documentation/client-snippets/projections/model-bound/event-sequence-source/fluent.md create mode 100644 Documentation/client-snippets/projections/model-bound/from-every/combined.md create mode 100644 Documentation/client-snippets/projections/model-bound/from-every/context-fields.md create mode 100644 Documentation/client-snippets/projections/model-bound/from-every/context-property.md create mode 100644 Documentation/client-snippets/projections/model-bound/from-every/convention.md create mode 100644 Documentation/client-snippets/projections/model-bound/from-every/declarative-equivalent.md create mode 100644 Documentation/client-snippets/projections/model-bound/from-every/event-property.md create mode 100644 Documentation/client-snippets/projections/model-bound/index/automap-fluent.md create mode 100644 Documentation/client-snippets/projections/model-bound/index/automap-model-bound.md create mode 100644 Documentation/client-snippets/projections/model-bound/index/basic.md create mode 100644 Documentation/client-snippets/projections/model-bound/index/explicit-mapping-fluent.md create mode 100644 Documentation/client-snippets/projections/model-bound/index/explicit-mapping-model-bound.md create mode 100644 Documentation/client-snippets/projections/model-bound/joins/basic.md create mode 100644 Documentation/client-snippets/projections/model-bound/joins/children.md create mode 100644 Documentation/client-snippets/projections/model-bound/joins/full-example.md create mode 100644 Documentation/client-snippets/projections/model-bound/joins/multiple-join-sources.md create mode 100644 Documentation/client-snippets/projections/model-bound/joins/multiple-joins.md create mode 100644 Documentation/client-snippets/projections/model-bound/not-rewindable/basic.md create mode 100644 Documentation/client-snippets/projections/model-bound/passive/basic.md create mode 100644 Documentation/client-snippets/projections/model-bound/removal/basic.md create mode 100644 Documentation/client-snippets/projections/model-bound/removal/full-example.md create mode 100644 Documentation/client-snippets/projections/model-bound/removal/join-on-class.md create mode 100644 Documentation/client-snippets/projections/model-bound/removal/multiple-triggers.md create mode 100644 Documentation/client-snippets/projections/model-bound/removal/with-key.md create mode 100644 Documentation/client-snippets/projections/model-bound/set-value/invoice.md create mode 100644 Documentation/client-snippets/projections/model-bound/set-value/order.md create mode 100644 Documentation/client-snippets/projections/model-bound/set-value/subscription.md create mode 100644 Documentation/client-snippets/projections/model-bound/set-value/thing.md create mode 100644 Documentation/client-snippets/projections/nested-objects-design/declarative.md create mode 100644 Documentation/client-snippets/projections/nested-objects-design/model-bound.md create mode 100644 Documentation/client-snippets/projections/nested-objects-design/recursive.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/auto-map/event.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/auto-map/read-model.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/children/read-model.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/counters/read-model.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/expressions/read-model.md create mode 100644 Documentation/client-snippets/projections/projection-declaration-language/nested/read-model.md create mode 100644 Documentation/client-snippets/reactors/event-processing/event-context.md create mode 100644 Documentation/client-snippets/reactors/event-sequence/reactor-attribute.md create mode 100644 Documentation/client-snippets/reactors/getting-started/event.md create mode 100644 Documentation/client-snippets/reactors/getting-started/reactor.md create mode 100644 Documentation/client-snippets/reactors/getting-started/register.md create mode 100644 Documentation/client-snippets/reactors/getting-started/stable-id.md create mode 100644 Documentation/client-snippets/reactors/index/basic-example.md create mode 100644 Documentation/client-snippets/read-models/getting-collection-instances/basic.md create mode 100644 Documentation/client-snippets/read-models/getting-collection-instances/event-count.md create mode 100644 Documentation/client-snippets/read-models/getting-collection-instances/filtering.md create mode 100644 Documentation/client-snippets/read-models/getting-single-instance/basic.md create mode 100644 Documentation/client-snippets/read-models/getting-snapshots/analyze.md create mode 100644 Documentation/client-snippets/read-models/getting-snapshots/basic.md create mode 100644 Documentation/client-snippets/read-models/materialized-pagination/accessing-api.md create mode 100644 Documentation/client-snippets/read-models/materialized-pagination/basic-usage.md create mode 100644 Documentation/client-snippets/read-models/materialized-pagination/observing.md create mode 100644 Documentation/client-snippets/read-models/materialized-pagination/pagination.md create mode 100644 Documentation/client-snippets/read-models/watching-read-models/basic.md create mode 100644 Documentation/client-snippets/read-models/watching-read-models/filtering.md create mode 100644 Documentation/client-snippets/reducers/event-sequence/reducer-attribute.md create mode 100644 Documentation/client-snippets/reducers/getting-started/read-model.md create mode 100644 Documentation/client-snippets/reducers/getting-started/reducer-attribute.md create mode 100644 Documentation/client-snippets/reducers/getting-started/reducer-implementation.md create mode 100644 Documentation/client-snippets/reducers/getting-started/retrieving-state.md create mode 100644 Documentation/client-snippets/reducers/index/basic-example.md create mode 100644 Documentation/client-snippets/scenarios/react-to-an-event/basic-reactor.md create mode 100644 Documentation/client-snippets/scenarios/react-to-an-event/explicit-append.md create mode 100644 Documentation/client-snippets/scenarios/react-to-an-event/reading-state.md create mode 100644 Documentation/client-snippets/scenarios/real-time-query/get-one.md create mode 100644 Documentation/client-snippets/subscriptions/explicit-subscriptions/basic.md create mode 100644 Documentation/client-snippets/subscriptions/explicit-subscriptions/filtering.md create mode 100644 Documentation/client-snippets/subscriptions/explicit-subscriptions/inbox-reactor.md create mode 100644 Documentation/client-snippets/subscriptions/explicit-subscriptions/naming-convention.md create mode 100644 Documentation/client-snippets/subscriptions/explicit-subscriptions/no-filter.md create mode 100644 Documentation/client-snippets/subscriptions/explicit-subscriptions/unsubscribe.md create mode 100644 Documentation/client-snippets/subscriptions/outbox-inbox/inbox-reactor.md create mode 100644 Documentation/client-snippets/tutorial/first-event/append.md create mode 100644 Documentation/client-snippets/tutorial/first-event/book-added.md create mode 100644 Documentation/client-snippets/tutorial/first-event/book-id.md create mode 100644 Documentation/client-snippets/tutorial/reacting/explicit-append.md create mode 100644 Documentation/client-snippets/tutorial/reacting/notification-sent-event.md create mode 100644 Documentation/client-snippets/tutorial/reacting/once-only.md create mode 100644 Documentation/client-snippets/tutorial/reacting/reading-state.md create mode 100644 Documentation/client-snippets/tutorial/reacting/side-effect.md create mode 100644 Documentation/client-snippets/tutorial/reacting/waitlist-notifier.md create mode 100644 Documentation/client-snippets/tutorial/read-model/a-events.md create mode 100644 Documentation/client-snippets/tutorial/read-model/book.md create mode 100644 Documentation/client-snippets/tutorial/read-model/borrowed-book.md create mode 100644 Documentation/client-snippets/understanding-constraints/dedicated-class.md create mode 100644 Documentation/client-snippets/webhooks/index/query.md create mode 100644 Documentation/client-snippets/webhooks/index/register.md create mode 100644 Documentation/projections.md create mode 100644 Documentation/toc.yml create mode 100644 Documentation/validate-client-snippets.py diff --git a/.github/workflows/client-snippets.yml b/.github/workflows/client-snippets.yml new file mode 100644 index 0000000..cdb74ab --- /dev/null +++ b/.github/workflows/client-snippets.yml @@ -0,0 +1,58 @@ +name: Client Snippet Verification + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + + pull_request: + branches: + - "**" + paths: + - "Documentation/client-snippets/**" + - "Documentation/validate-client-snippets.py" + - "Source/**" + - ".github/workflows/client-snippets.yml" + + push: + branches: ["main"] + paths: + - "Documentation/client-snippets/**" + - "Documentation/validate-client-snippets.py" + - "Source/**" + - ".github/workflows/client-snippets.yml" + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 23.x + registry-url: "https://registry.npmjs.org" + + - uses: actions/cache@v4 + id: yarn-cache + with: + path: | + .yarn/cache + **/node_modules + **/.eslintcache + **/yarn.lock + key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }} + + - name: Yarn install + run: yarn + + - name: Compile client snippets + run: python3 Documentation/validate-client-snippets.py diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..0fe1b96 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,27 @@ +name: Trigger documentation build + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + + push: + branches: ["main"] + paths: + - "Documentation/**" + +permissions: + contents: read + +jobs: + trigger: + runs-on: ubuntu-latest + steps: + - name: Trigger Documentation Build + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.PAT_DOCUMENTATION }} + repository: Cratis/Documentation + event-type: build-docs diff --git a/Documentation/__pycache__/validate-client-snippets.cpython-314.pyc b/Documentation/__pycache__/validate-client-snippets.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad25d264244ad749cda968993aff4b5882443cba GIT binary patch literal 12779 zcmc&aX>1!;dT+?#ed#_aadg-+Es3;cIhN)4kR{qyBumOjrgKPUXmTVorbuRH7Rb@4Tq#< z*>QpveZai;<{jVrzIS}rd);=M8A0e;IQsnVHiUkQf7IYCWWM}LgHQ^cLM%0kL?xv} ziu`Lt4N0}4mZY>ulT;__Ak~cOj~PUR3(XnY(44WKQXo!Mt3#C1{z7)Drc|cpPEkEg zY91lcGG}46J(jW(E0oaXC9JOePqfWhS^YgF_BlIixTnMcCB}P7D(0xE_R=V7t_`s! zsOePyOKCsM6Aoha1ACg)IS^|e*v3+9Q~SU+)?%Bw2ez>l+tjmmv4Pz!HnI-3f~{n$ z*lMGs&G0!P-vVW=kh+mymqn4gV@I=DY2PtuSaZ0J&IO{ zPPP;BTS&fz-3s|Gl5b_VLEcUBE_OTQyGfp5dm!IS@@=dK@?Mf}XLmq;C&_orb+UZ} zPaza`Aml{NXl@JaosKr7?huu#GZag8AO_8C)uZqh+*a(G+Xkz%Yp%G@K@rNm=Xbd4gqxD-si1g9)=$Tm_z)-CM&hBR zMJ^@=#YjAsbcCW2_~{j5kwk(Mg{0jRj)#Qe|92L(#sif$Q3%Cj^N|J5^8$2keeSvE zwmo}lR(Ni%+YM=KZu@L(n`iq$_iPNRl7~)3787w^oE5rfUxbD)L9^MH;BTIfFGBKc z*KGUTp%4$K2;R^cJ|2sNq8usRcW_qN-kBWxQ4nT5fThR>L!wX)Z1{%&n_r5BgJhnf z_WDdf&C?1V+A-qwG z(1il{zDI69ABl<_4>%^bY(Pmo!*jx!cofgq&K)~;6o~A9WYA>NdkJ01j?H=rv0ws5 zfVEU6W%~vU$RcgNUdb&F2N;Y6qZgB0a%*|B^Fc9m2CqqZVPWXZSmFe+7a$3MnUEa(LEQz8KNkscy$YdN;uoYo(n218Uk*Z)LVk2gGw{d2 z=b_Aro}@58pM@GF%oeybLgljG3mpGF&F^rXtmip#iI2H8G98TwB9B1>r`FHM`Ng0p z;8EM=}gyKWxJ# zjSId6iXgWxNpJTohLbuRUr08NEG43m5bOa)*&<9q{4+_-Ad}QF!C07an`FHJLX8W{ z*6E4SsVUz~fSsJ2k#+n+G=5rERffP>?xJiaB2G9T0UgBY9~vJW8JZcLoCx@y8Xi9} z;u{G}vA!dtPx+>0vp*PJ;(R>0E#6pp`Lq@h0Dg&e~?}9S2qyH5$DfE>d)iz7b{wuTD7H_)6dqbaT*#rN- z?#T|0rw7MBI++BUh6rQb&P(aLnhlY z{c+97ybkHic?6B~W>is?I`&CL?J~V?Z@k=@vAdSFIg9PMUctI}=XNgx9!OW+pC8C#04aGF7}}<#P?rY{Ry6!?yPuGYz|zjj!46nowmO z3}CHHjeq+^J&Y#cW4F3>*hYWQp@%qxkDYq-@SKn%xr31XaL&bQN{7#@=|FBRkBig$T0_itkd&0rhV~>B(VhoF9<*XC7~p#fgeJa+!~2VTJ)tS%u2*<8}?l3fa% zC4pmtOsO0IhYOH;m<2FxKn$3$P8q-;WaAOv#IP^G`eXwrfZ<>?Dr=)$jBfw}l!Jx+ zAac`*iRlwlQfGb>xuAT0VGQHh?+=rVnuXXfU?01Uj8e zV>|@B>$aA7y#r9d_FkaC^Z7kcPqy?m$iR^_+b?-u@m%d%_GHZ4Q^V`_x~#o5ZEw9S zWb7TOqw5Cq@}58XgOwN73=Qk{hRY*aXHVMM^ZwHrCkTZBY2d8X7|qxhCFA0qTIb~_ z-+bl|CsN092FJ={Ylgq`eq%2DnOJm-I|IDbV z<;wIg8uAwWXGMC`T^r<-Rm7W94SL9sRlEst3KZdlZgZig9`6d|qmH}>%B8+E*PDqq ze^C`o82kn-d2k4yz^ut{*i7V={6(87!)%f5f^rM0Yr^O^ZpKnoJaN4`>H^T_L}M+W zE>lxQg;&7{$Nr-K)P=@C5y8sz*b?YtMaKF)6q@`db*ut~rvEnzHIGc;e*lF=CH{LT zH2cj(3jJga&Hrx{Y9EzCA2kMM&}@~u(%(a)#cwIn*hy%lHyz3HaLQFd>4K9ob{Te; zZVGE)8$#)Pkaq7YyN=aNVT*P{@at5V58LrrAO0QY&N9^_Q)agnj2p0)#AqZ263Q_# zbR1;b=y5FEupHD8*(K{1`1n!+gd1+k_u=l^U^px{4o^j?BAGrN4`0NN zhZrYY6vnGyYE&3An02)qIkVEBVH3Wr)Kr#pWl|(y@wF8PETti+Mj4)lBICwY!RW6Nh2CghWv z?8(;jrEB_b4BW1H90MMuz91MEv7-TqWNiG*;>=lHS!+ky+97rB&UWrgckWvq z%5)xBvmTVKHFu2GOO{tGDG~u=a}P9sS#S$>qIKePc1xFmmew zCLzX}tO9UL==pFJ5IY!oY=Q76A$r&h{}t2=*v`e)c{gRai=!(ruaGEP0qTmu2swg; zpT<2(PQ9Wg0Enr4eF4CN!Bm)1gckvNDFNSXseBfDOm|RV3L4bfBD6B2uUMvKHPD)# z#ki0FEz)lZR$AS>T?%defoqBbTxw?fj0CB)`n(_ zCRs_FNtkBwG?GwO3bK42W&kuIa5~KvWz)%bPj>j}{(^Rxu=q{0b!u;7R=z>zuIQB9 zoRU%>zo}fme-!!>)|3CdO9OLfE6g3}UoU)GTssA4m_NS_W&$)9;`Fp9Y-O{H?YRvB z4w7~X@9ho2Z&z_*Y4v_N;Q-wf^u&#U)y-lD9SMGu+7FC%z25<`p|7}pph5l)5Z;EMQP8mZ`rC>zOMKGNo^ z%{n}kzjBiD>)|uNyj4sT=cVX!)9Bu*5526wd` zo*a2v_18?xdQ3kw9mlZ8aD-(XkL>_=74O9P%`{I-m!rORZ;f%`K_X-;&FMhK{QvnFjZ=ZM~}PYGjMX*gY4SCbjCE39+*zi$6T{A^VZX^ zKYiug)y0gfKU2F~s(DO$>f2x@|;wEe$9B{Geg@Z4JR|6sZ8gSQU@!syi_l&8O1x6%1irR*_Sgunb+zY z^mj}Zc^#yAJu=tj4LD~+=IXo&=gi1t$y*@zCDL0Bd8F4FzWrh^az&|czi2|W2fzKo zj?5=00gK2F486yk^l!G9j$wPUyaNh}Sl(4x*4H_!Vn$>4YtYw2v zZIuncZQxCdkgNbMde3Vj*%68_CV>0#lL^cq1X&-8`xYbOjBJm^PsG3<9UhN|f>Gg! zT%{IF0XG3D!0`f{_E?Cp2W$p__RB%S58*uAvg7AUvM#)|m=N$iJD9idh>6_e6ztN< zA{$VV;$MWmWGatBCWY=8E2Zi!Ib&naTC)<)!2#iNR_5xKyn%L_@!qIfIlgi*?=af+ zsUu%iVM}Z9)xnkdt52?3x~_NLvH(Ny+;3?8xIF+wXd|(Q`Z@m-ur4b37Rq)PLG;rQ zN$%eT253!oL5djSW$TPfHw4%t_&x`t4#8EX;&0}-Dcy>wYBt_qWxY!ZFUDd+DBYiL z-n+TniCD3rGyJxJ%asLnq~xRm-03|9ssQ&Iwa-*h8COei@6t>_KV2EtjcO~l=!kW} z15^C0@5jO{KHf9Vz@{}Mf;@f(23I^6;E_lmewN<`WAb33;61Gkoe#@8xEu+ch23C_ zEW|)J3UK)PTuAOKyXq;v3%O6fDjc{lyiyCnyBlGCDH^@#aqDFIVvvu?GAd%4;Jv5UZIf+BeG@)VE5HdhG77gji)X_TUalyWPEVd-hkdg7JRgBKCRpzv z!d9*-HJOXbUtDjkWhLaXwxMk!& zH`*`RUbVfkc%%Q8ao}^>B$g*4fA4BNydRtm zfdED}5J{&EUD zfUrU+o(l4~a%d4hiiBqO%)Wa&e*qu$XK{21M?ZxK+)Wn+{xr@3ZxGK!Bd3+u7_fKn zZU_Wq13bZtN6&Hm%eWK1apnIEN5lyDQ=EGRN3TI78w$r?){jhrqj{Y7;HqI99mCNm zj-J5LP8?ykIUmMR5J%Wr-(V#uMgZ2I$?+xU4)1HcHvl*D^hH7B7JU~YqHOYA2ysNr z%R0ic_}8HuJS!4mxq1F)7zSqM$}u)RuBi0~_;;ZU1QZhf1>(GxqNp#;h_ZfyOrIdz zC#dohWc~y>K1B_8^~hGa((u|u*4m!7wr8xJS!++)+H?KMTUKw%l(W}g-ge90p0eZ| z4VMqza%`7+58QGbNLh0g%~Gr9R)r^J%T+U1_us1CA)$&~ZTr>0t=fJGRpl%;EZC*G zJ*!oJ*Z4P$8Ow0Wn5(LJYx~c(OD)f3s)8v;&e{6T6K_8ubuDF_=OomabG5ybd^;&^ zPh?y#NXVIMXnM!|wpr>J%``kAq1s%_kc66Yu5PFU)l=7cna|PN;3-kJ5i|?kjy%%z z8F2-?BgxSN@Vk%v?pr;aquub^Lw(CB&+eLoIsE?rGv5(qw^zcnKM<2S` zp5NP2sY%(sIM7ZO!gc1?ewblg0FJ@0XDLu4zit98 zBJV`Bbve1FZOm(Eo%3UR!_RB}qAp`+z}COM0*FmfAh|>HI`)xie`p|0d?a_W%?eg_$KkU;%{(k@h CXwzf> literal 0 HcmV?d00001 diff --git a/Documentation/auditing.md b/Documentation/auditing.md index 7335da3..0dacd44 100644 --- a/Documentation/auditing.md +++ b/Documentation/auditing.md @@ -1,6 +1,6 @@ # Auditing — Causation -Causation records *why* an event was appended — a chain of reasons from the root of a business operation down to the individual command or handler that triggered the append. This chain is stored with every event and enables full audit-trail tracing. +See [Correlation, identity, and causation](/chronicle/concepts/correlation-identity-causation/) for what causation means and why Chronicle tracks it. This page covers the TypeScript-specific API in depth. The causation chain is stored with every event and enables full audit-trail tracing. ## Concepts diff --git a/Documentation/client-snippets/coming-from-crud/events.md b/Documentation/client-snippets/coming-from-crud/events.md new file mode 100644 index 0000000..e2a85f6 --- /dev/null +++ b/Documentation/client-snippets/coming-from-crud/events.md @@ -0,0 +1,13 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class CrudComparisonCustomerRegistered { + constructor(readonly name: string, readonly address: string) {} +} + +@eventType() +class CrudComparisonAddressChanged { + constructor(readonly address: string) {} +} +``` diff --git a/Documentation/client-snippets/coming-from-crud/read-model.md b/Documentation/client-snippets/coming-from-crud/read-model.md new file mode 100644 index 0000000..64d4105 --- /dev/null +++ b/Documentation/client-snippets/coming-from-crud/read-model.md @@ -0,0 +1,15 @@ +```typescript +import { count, fromEvent, Guid, readModel } from '@cratis/chronicle'; + +@readModel() +@fromEvent(CrudComparisonCustomerRegistered) +@fromEvent(CrudComparisonAddressChanged) +class CrudComparisonCustomerCard { + id: Guid = Guid.empty; + name = ''; + address = ''; + + @count(CrudComparisonAddressChanged) + timesRelocated = 0; +} +``` diff --git a/Documentation/client-snippets/coming-from-crud/write-and-read.md b/Documentation/client-snippets/coming-from-crud/write-and-read.md new file mode 100644 index 0000000..30b8265 --- /dev/null +++ b/Documentation/client-snippets/coming-from-crud/write-and-read.md @@ -0,0 +1,12 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class CrudComparisonCustomerAddressUpdater { + constructor(private readonly store: IEventStore) {} + + async changeAddress(customerId: string, newAddress: string): Promise { + await this.store.eventLog.append(customerId, new CrudComparisonAddressChanged(newAddress)); + return this.store.readModels.getInstanceById(CrudComparisonCustomerCard, customerId); + } +} +``` diff --git a/Documentation/client-snippets/compliance/client/concept-annotation.md b/Documentation/client-snippets/compliance/client/concept-annotation.md new file mode 100644 index 0000000..cdd2897 --- /dev/null +++ b/Documentation/client-snippets/compliance/client/concept-annotation.md @@ -0,0 +1,11 @@ +```typescript +import { pii } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +@pii() +class ComplianceClientPersonName extends ConceptAs { + constructor(value: string) { + super(value); + } +} +``` diff --git a/Documentation/client-snippets/compliance/client/property-annotation.md b/Documentation/client-snippets/compliance/client/property-annotation.md new file mode 100644 index 0000000..0e26f95 --- /dev/null +++ b/Documentation/client-snippets/compliance/client/property-annotation.md @@ -0,0 +1,11 @@ +```typescript +import { eventType, pii } from '@cratis/chronicle'; + +@eventType() +class ComplianceClientEmployeeRegistered { + @pii() firstName = ''; + @pii() lastName = ''; + department = ''; + startDate = new Date(); +} +``` diff --git a/Documentation/client-snippets/compliance/pii-with-concepts/defining-concept.md b/Documentation/client-snippets/compliance/pii-with-concepts/defining-concept.md new file mode 100644 index 0000000..98f6b50 --- /dev/null +++ b/Documentation/client-snippets/compliance/pii-with-concepts/defining-concept.md @@ -0,0 +1,11 @@ +```typescript +import { pii } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +@pii() +class PiiConceptsPersonName extends ConceptAs { + constructor(value: string) { + super(value); + } +} +``` diff --git a/Documentation/client-snippets/compliance/pii-with-concepts/national-id.md b/Documentation/client-snippets/compliance/pii-with-concepts/national-id.md new file mode 100644 index 0000000..97f0a69 --- /dev/null +++ b/Documentation/client-snippets/compliance/pii-with-concepts/national-id.md @@ -0,0 +1,11 @@ +```typescript +import { pii } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +@pii('National ID number — sensitive personal identifier') +class PiiConceptsNationalIdNumber extends ConceptAs { + constructor(value: string) { + super(value); + } +} +``` diff --git a/Documentation/client-snippets/compliance/pii-with-concepts/with-details.md b/Documentation/client-snippets/compliance/pii-with-concepts/with-details.md new file mode 100644 index 0000000..552883b --- /dev/null +++ b/Documentation/client-snippets/compliance/pii-with-concepts/with-details.md @@ -0,0 +1,11 @@ +```typescript +import { pii } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +@pii('Collected under GDPR Art. 6(1)(b) — necessary for contract performance. Retention: contract duration + 7 years.') +class PiiConceptsLegalName extends ConceptAs { + constructor(value: string) { + super(value); + } +} +``` diff --git a/Documentation/client-snippets/compliance/pii/concept-level.md b/Documentation/client-snippets/compliance/pii/concept-level.md new file mode 100644 index 0000000..f9e0e70 --- /dev/null +++ b/Documentation/client-snippets/compliance/pii/concept-level.md @@ -0,0 +1,11 @@ +```typescript +import { pii } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +@pii() +class PiiAttrConceptPersonName extends ConceptAs { + constructor(value: string) { + super(value); + } +} +``` diff --git a/Documentation/client-snippets/compliance/pii/legal-name-details.md b/Documentation/client-snippets/compliance/pii/legal-name-details.md new file mode 100644 index 0000000..096b819 --- /dev/null +++ b/Documentation/client-snippets/compliance/pii/legal-name-details.md @@ -0,0 +1,11 @@ +```typescript +import { pii } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +@pii('Full legal name — required for contract identification') +class PiiAttrLegalName extends ConceptAs { + constructor(value: string) { + super(value); + } +} +``` diff --git a/Documentation/client-snippets/compliance/pii/property-level.md b/Documentation/client-snippets/compliance/pii/property-level.md new file mode 100644 index 0000000..9f72581 --- /dev/null +++ b/Documentation/client-snippets/compliance/pii/property-level.md @@ -0,0 +1,10 @@ +```typescript +import { eventType, pii } from '@cratis/chronicle'; + +@eventType() +class PiiAttrEmployeeRegistered { + @pii() firstName = ''; + @pii() lastName = ''; + department = ''; +} +``` diff --git a/Documentation/client-snippets/compliance/pii/with-details.md b/Documentation/client-snippets/compliance/pii/with-details.md new file mode 100644 index 0000000..61d03b9 --- /dev/null +++ b/Documentation/client-snippets/compliance/pii/with-details.md @@ -0,0 +1,11 @@ +```typescript +import { pii } from '@cratis/chronicle'; +import { ConceptAs } from '@cratis/fundamentals'; + +@pii('Collected under GDPR Art. 6(1)(b) — necessary for contract performance') +class PiiAttrPersonName extends ConceptAs { + constructor(value: string) { + super(value); + } +} +``` diff --git a/Documentation/client-snippets/concepts/correlation-identity-causation/causation.md b/Documentation/client-snippets/concepts/correlation-identity-causation/causation.md new file mode 100644 index 0000000..6a66248 --- /dev/null +++ b/Documentation/client-snippets/concepts/correlation-identity-causation/causation.md @@ -0,0 +1,9 @@ +```typescript +import { causationManager, CausationType } from '@cratis/chronicle'; + +class CorrelationIdentityCausationCausation { + recordPlaceOrder(orderId: string): void { + causationManager.add(new CausationType('MyApp.Commands.PlaceOrder'), { orderId }); + } +} +``` diff --git a/Documentation/client-snippets/concepts/correlation-identity-causation/correlation.md b/Documentation/client-snippets/concepts/correlation-identity-causation/correlation.md new file mode 100644 index 0000000..a84f22c --- /dev/null +++ b/Documentation/client-snippets/concepts/correlation-identity-causation/correlation.md @@ -0,0 +1,13 @@ +```typescript +import { correlationIdManager, CorrelationId } from '@cratis/chronicle'; + +class CorrelationIdentityCausationCorrelation { + getCurrent(): CorrelationId { + return correlationIdManager.current; + } + + setForRequest(): void { + correlationIdManager.setCurrent(CorrelationId.create()); + } +} +``` diff --git a/Documentation/client-snippets/concepts/correlation-identity-causation/identity.md b/Documentation/client-snippets/concepts/correlation-identity-causation/identity.md new file mode 100644 index 0000000..1127700 --- /dev/null +++ b/Documentation/client-snippets/concepts/correlation-identity-causation/identity.md @@ -0,0 +1,13 @@ +```typescript +import { identityProvider, Identity } from '@cratis/chronicle'; + +class CorrelationIdentityCausationIdentity { + setForRequest(subject: string, name: string, userName: string): void { + identityProvider.setCurrentIdentity(new Identity(subject, name, userName)); + } + + getCurrent(): Identity { + return identityProvider.getCurrent(); + } +} +``` diff --git a/Documentation/client-snippets/concepts/designing-read-models/query-ladder.md b/Documentation/client-snippets/concepts/designing-read-models/query-ladder.md new file mode 100644 index 0000000..7751862 --- /dev/null +++ b/Documentation/client-snippets/concepts/designing-read-models/query-ladder.md @@ -0,0 +1,24 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class DesigningReadModelsCustomerListItem { + constructor( + readonly id: string, + readonly name: string + ) {} +} + +class DesigningReadModelsCustomerListService { + constructor(private readonly store: IEventStore) {} + + // Strongly consistent — Chronicle replays the read model's events on demand + getAllStronglyConsistent(): Promise { + return this.store.readModels.getInstances(DesigningReadModelsCustomerListItem); + } + + // Eventually consistent — a page of materialized instances straight from storage + getPageEventuallyConsistent(): Promise { + return this.store.readModels.materialized.getInstances(DesigningReadModelsCustomerListItem, 0, 20); + } +} +``` diff --git a/Documentation/client-snippets/concepts/designing-read-models/strongly-consistent.md b/Documentation/client-snippets/concepts/designing-read-models/strongly-consistent.md new file mode 100644 index 0000000..b657b9b --- /dev/null +++ b/Documentation/client-snippets/concepts/designing-read-models/strongly-consistent.md @@ -0,0 +1,18 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class DesigningReadModelsCustomerDetail { + constructor( + readonly id: string, + readonly name: string + ) {} +} + +class DesigningReadModelsCustomerDetailService { + constructor(private readonly store: IEventStore) {} + + getDetail(customerId: string): Promise { + return this.store.readModels.getInstanceById(DesigningReadModelsCustomerDetail, customerId); + } +} +``` diff --git a/Documentation/client-snippets/concepts/event-type-migrations/combine.md b/Documentation/client-snippets/concepts/event-type-migrations/combine.md new file mode 100644 index 0000000..399dfea --- /dev/null +++ b/Documentation/client-snippets/concepts/event-type-migrations/combine.md @@ -0,0 +1,30 @@ +```typescript +import { eventType, eventTypeMigration, IEventTypeMigration, IEventMigrationBuilder } from '@cratis/chronicle'; + +@eventType() +class MigrationsCombineShippingAddressRecordedV1 { + constructor( + readonly street: string, + readonly city: string + ) {} +} + +@eventType('shipping-address-recorded', 2) +class MigrationsCombineShippingAddressRecorded { + constructor(readonly formattedAddress: string) {} +} + +@eventTypeMigration(MigrationsCombineShippingAddressRecorded, MigrationsCombineShippingAddressRecordedV1) +class MigrationsCombineShippingAddressRecordedMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(pb => pb + .combine('formattedAddress', ' ', 'street', 'city')); // Joins with space separator + } + + downcast(builder: IEventMigrationBuilder): void { + builder.properties(pb => pb + .split('street', 'formattedAddress', ' ', 0) + .split('city', 'formattedAddress', ' ', 1)); + } +} +``` diff --git a/Documentation/client-snippets/concepts/event-type-migrations/default-value.md b/Documentation/client-snippets/concepts/event-type-migrations/default-value.md new file mode 100644 index 0000000..ff0de58 --- /dev/null +++ b/Documentation/client-snippets/concepts/event-type-migrations/default-value.md @@ -0,0 +1,30 @@ +```typescript +import { eventType, eventTypeMigration, IEventTypeMigration, IEventMigrationBuilder } from '@cratis/chronicle'; + +@eventType() +class MigrationsDefaultValueOrderShippedV1 { + constructor(readonly trackingNumber: string) {} +} + +@eventType('order-shipped', 2) +class MigrationsDefaultValueOrderShipped { + constructor( + readonly trackingNumber: string, + readonly retryCount: number, + readonly description: string + ) {} +} + +@eventTypeMigration(MigrationsDefaultValueOrderShipped, MigrationsDefaultValueOrderShippedV1) +class MigrationsDefaultValueOrderShippedMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(pb => pb + .defaultValue('retryCount', 42) + .defaultValue('description', 'default string')); + } + + downcast(_builder: IEventMigrationBuilder): void { + // retryCount and description did not exist in generation 1 — nothing to map back + } +} +``` diff --git a/Documentation/client-snippets/concepts/event-type-migrations/defining-migrations.md b/Documentation/client-snippets/concepts/event-type-migrations/defining-migrations.md new file mode 100644 index 0000000..2502454 --- /dev/null +++ b/Documentation/client-snippets/concepts/event-type-migrations/defining-migrations.md @@ -0,0 +1,30 @@ +```typescript +import { eventType, eventTypeMigration, IEventTypeMigration, IEventMigrationBuilder } from '@cratis/chronicle'; + +@eventType() +class MigrationsAuthorRegisteredV1 { + constructor(readonly name: string) {} +} + +@eventType('author-registered', 2) +class MigrationsAuthorRegistered { + constructor( + readonly firstName: string, + readonly lastName: string + ) {} +} + +@eventTypeMigration(MigrationsAuthorRegistered, MigrationsAuthorRegisteredV1) +class MigrationsAuthorRegisteredMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(pb => pb + .split('firstName', 'name', ' ', 0) + .split('lastName', 'name', ' ', 1)); + } + + downcast(builder: IEventMigrationBuilder): void { + builder.properties(pb => pb + .combine('name', ' ', 'firstName', 'lastName')); + } +} +``` diff --git a/Documentation/client-snippets/concepts/event-type-migrations/rename.md b/Documentation/client-snippets/concepts/event-type-migrations/rename.md new file mode 100644 index 0000000..32005cc --- /dev/null +++ b/Documentation/client-snippets/concepts/event-type-migrations/rename.md @@ -0,0 +1,26 @@ +```typescript +import { eventType, eventTypeMigration, IEventTypeMigration, IEventMigrationBuilder } from '@cratis/chronicle'; + +@eventType() +class MigrationsRenamePaymentProcessedV1 { + constructor(readonly oldAmount: number) {} +} + +@eventType('payment-processed', 2) +class MigrationsRenamePaymentProcessed { + constructor(readonly amount: number) {} +} + +@eventTypeMigration(MigrationsRenamePaymentProcessed, MigrationsRenamePaymentProcessedV1) +class MigrationsRenamePaymentProcessedMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(pb => pb + .renamedFrom('amount', 'oldAmount')); + } + + downcast(builder: IEventMigrationBuilder): void { + builder.properties(pb => pb + .renamedFrom('oldAmount', 'amount')); + } +} +``` diff --git a/Documentation/client-snippets/concepts/event-type-migrations/split.md b/Documentation/client-snippets/concepts/event-type-migrations/split.md new file mode 100644 index 0000000..93cf86d --- /dev/null +++ b/Documentation/client-snippets/concepts/event-type-migrations/split.md @@ -0,0 +1,30 @@ +```typescript +import { eventType, eventTypeMigration, IEventTypeMigration, IEventMigrationBuilder } from '@cratis/chronicle'; + +@eventType() +class MigrationsSplitPersonRegisteredV1 { + constructor(readonly fullName: string) {} +} + +@eventType('person-registered', 2) +class MigrationsSplitPersonRegistered { + constructor( + readonly firstName: string, + readonly lastName: string + ) {} +} + +@eventTypeMigration(MigrationsSplitPersonRegistered, MigrationsSplitPersonRegisteredV1) +class MigrationsSplitPersonRegisteredMigration implements IEventTypeMigration { + upcast(builder: IEventMigrationBuilder): void { + builder.properties(pb => pb + .split('firstName', 'fullName', ' ', 0) // Gets first part + .split('lastName', 'fullName', ' ', 1)); // Gets second part + } + + downcast(builder: IEventMigrationBuilder): void { + builder.properties(pb => pb + .combine('fullName', ' ', 'firstName', 'lastName')); + } +} +``` diff --git a/Documentation/client-snippets/concepts/modeling-events/fact-vs-intent.md b/Documentation/client-snippets/concepts/modeling-events/fact-vs-intent.md new file mode 100644 index 0000000..ea37518 --- /dev/null +++ b/Documentation/client-snippets/concepts/modeling-events/fact-vs-intent.md @@ -0,0 +1,22 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +class ModelingEventsAddress { + constructor( + readonly street: string, + readonly city: string + ) {} +} + +// A fact that happened +@eventType() +class ModelingEventsAddressChanged { + constructor(readonly address: ModelingEventsAddress) {} +} + +// An intent (that's a command) or a state blob (that's a read model) — not an event +@eventType() +class ModelingEventsUpdateAddress { + constructor(readonly address: ModelingEventsAddress) {} +} +``` diff --git a/Documentation/client-snippets/concepts/modeling-events/no-nullable.md b/Documentation/client-snippets/concepts/modeling-events/no-nullable.md new file mode 100644 index 0000000..d2e1661 --- /dev/null +++ b/Documentation/client-snippets/concepts/modeling-events/no-nullable.md @@ -0,0 +1,41 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +class ModelingEventsOrderId { + constructor(readonly value: string) {} +} + +class ModelingEventsMoney { + constructor( + readonly amount: number, + readonly currency: string + ) {} +} + +// Nullable smell — "sometimes there's a discount, sometimes not" +@eventType() +class ModelingEventsOrderPlacedWithNullableDiscount { + constructor( + readonly id: ModelingEventsOrderId, + readonly total: ModelingEventsMoney, + readonly discount?: ModelingEventsMoney + ) {} +} + +// Two facts +@eventType() +class ModelingEventsOrderPlaced { + constructor( + readonly id: ModelingEventsOrderId, + readonly total: ModelingEventsMoney + ) {} +} + +@eventType() +class ModelingEventsDiscountApplied { + constructor( + readonly id: ModelingEventsOrderId, + readonly amount: ModelingEventsMoney + ) {} +} +``` diff --git a/Documentation/client-snippets/concepts/modeling-events/single-purpose.md b/Documentation/client-snippets/concepts/modeling-events/single-purpose.md new file mode 100644 index 0000000..890e1d8 --- /dev/null +++ b/Documentation/client-snippets/concepts/modeling-events/single-purpose.md @@ -0,0 +1,49 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +class ModelingEventsCustomerName { + constructor(readonly value: string) {} +} + +class ModelingEventsEmail { + constructor(readonly value: string) {} +} + +class ModelingEventsDeactivationReason { + constructor(readonly value: string) {} +} + +class ModelingEventsCustomerAddress { + constructor( + readonly street: string, + readonly city: string + ) {} +} + +// One event trying to be everything — consumers must guess what changed +@eventType() +class ModelingEventsCustomerUpdated { + constructor( + readonly name?: ModelingEventsCustomerName, + readonly address?: ModelingEventsCustomerAddress, + readonly email?: ModelingEventsEmail, + readonly deactivated?: boolean + ) {} +} + +// Distinct facts — each consumer subscribes to exactly what it cares about +@eventType() +class ModelingEventsCustomerRenamed { + constructor(readonly name: ModelingEventsCustomerName) {} +} + +@eventType() +class ModelingEventsCustomerAddressChanged { + constructor(readonly address: ModelingEventsCustomerAddress) {} +} + +@eventType() +class ModelingEventsCustomerDeactivated { + constructor(readonly reason: ModelingEventsDeactivationReason) {} +} +``` diff --git a/Documentation/client-snippets/concepts/subject/default.md b/Documentation/client-snippets/concepts/subject/default.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/subject/default.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/subject/explicit.md b/Documentation/client-snippets/concepts/subject/explicit.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/subject/explicit.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/concepts/tagging/dynamic-event-tags.md b/Documentation/client-snippets/concepts/tagging/dynamic-event-tags.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/concepts/tagging/dynamic-event-tags.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/declarative/index/defining.md b/Documentation/client-snippets/constraints/declarative/index/defining.md new file mode 100644 index 0000000..c4312bf --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/index/defining.md @@ -0,0 +1,22 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsDeclarativeIndexProjectCreated { + constructor(readonly name: string) {} +} + +@eventType() +class ConstraintsDeclarativeIndexProjectRemoved { +} + +@constraint() +class ConstraintsDeclarativeIndexUniqueProjectName implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .on(ConstraintsDeclarativeIndexProjectCreated, e => e.name) + .removedWith(ConstraintsDeclarativeIndexProjectRemoved)); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique-event-type/basic.md b/Documentation/client-snippets/constraints/declarative/unique-event-type/basic.md new file mode 100644 index 0000000..2fd4ca4 --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique-event-type/basic.md @@ -0,0 +1,14 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueEventTypeProjectInitialized { +} + +@constraint() +class ConstraintsUniqueEventTypeProjectInitialization implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.uniqueFor(ConstraintsUniqueEventTypeProjectInitialized); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique-event-type/constraint-name.md b/Documentation/client-snippets/constraints/declarative/unique-event-type/constraint-name.md new file mode 100644 index 0000000..c1b1b7c --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique-event-type/constraint-name.md @@ -0,0 +1,17 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueEventTypeNamedProjectInitialized { +} + +@constraint() +class ConstraintsUniqueEventTypeNamedProjectInitialization implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.uniqueFor( + ConstraintsUniqueEventTypeNamedProjectInitialized, + 'A project can only be initialized once.', + 'UniqueProjectInitialization'); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique-event-type/violation-message.md b/Documentation/client-snippets/constraints/declarative/unique-event-type/violation-message.md new file mode 100644 index 0000000..0f228b9 --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique-event-type/violation-message.md @@ -0,0 +1,14 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueEventTypeMessageProjectInitialized { +} + +@constraint() +class ConstraintsUniqueEventTypeMessageProjectInitialization implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.uniqueFor(ConstraintsUniqueEventTypeMessageProjectInitialized, 'A project can only be initialized once.'); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique/basic.md b/Documentation/client-snippets/constraints/declarative/unique/basic.md new file mode 100644 index 0000000..99c9e5c --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique/basic.md @@ -0,0 +1,22 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueProjectCreated { + constructor(readonly name: string) {} +} + +@eventType() +class ConstraintsUniqueProjectRemoved { +} + +@constraint() +class ConstraintsUniqueProjectName implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .on(ConstraintsUniqueProjectCreated, e => e.name) + .removedWith(ConstraintsUniqueProjectRemoved)); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique/constraint-name.md b/Documentation/client-snippets/constraints/declarative/unique/constraint-name.md new file mode 100644 index 0000000..a6d4ea4 --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique/constraint-name.md @@ -0,0 +1,18 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueNamedUserRegistered { + constructor(readonly email: string) {} +} + +@constraint() +class ConstraintsUniqueNamedEmail implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .withName('UniqueEmail') + .on(ConstraintsUniqueNamedUserRegistered, e => e.email)); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique/ignore-casing.md b/Documentation/client-snippets/constraints/declarative/unique/ignore-casing.md new file mode 100644 index 0000000..849e890 --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique/ignore-casing.md @@ -0,0 +1,18 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueCasingUserRegistered { + constructor(readonly email: string) {} +} + +@constraint() +class ConstraintsUniqueCasingEmail implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .on(ConstraintsUniqueCasingUserRegistered, e => e.email) + .ignoreCasing()); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique/multiple-event-types.md b/Documentation/client-snippets/constraints/declarative/unique/multiple-event-types.md new file mode 100644 index 0000000..71a4c7d --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique/multiple-event-types.md @@ -0,0 +1,29 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueUserRegistered { + constructor(readonly email: string) {} +} + +@eventType() +class ConstraintsUniqueUserEmailChanged { + constructor(readonly newEmail: string) {} +} + +@eventType() +class ConstraintsUniqueUserRemoved { +} + +@constraint() +class ConstraintsUniqueEmailAcrossEvents implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .withName('UniqueEmail') + .on(ConstraintsUniqueUserRegistered, e => e.email) + .on(ConstraintsUniqueUserEmailChanged, e => e.newEmail) + .removedWith(ConstraintsUniqueUserRemoved)); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique/releasing.md b/Documentation/client-snippets/constraints/declarative/unique/releasing.md new file mode 100644 index 0000000..40acc3d --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique/releasing.md @@ -0,0 +1,22 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueOrderPlaced { + constructor(readonly reference: string) {} +} + +@eventType() +class ConstraintsUniqueOrderCancelled { +} + +@constraint() +class ConstraintsUniqueOrderReference implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .on(ConstraintsUniqueOrderPlaced, e => e.reference) + .removedWith(ConstraintsUniqueOrderCancelled)); + } +} +``` diff --git a/Documentation/client-snippets/constraints/declarative/unique/violation-message.md b/Documentation/client-snippets/constraints/declarative/unique/violation-message.md new file mode 100644 index 0000000..360d003 --- /dev/null +++ b/Documentation/client-snippets/constraints/declarative/unique/violation-message.md @@ -0,0 +1,18 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class ConstraintsUniqueMessageProjectCreated { + constructor(readonly name: string) {} +} + +@constraint() +class ConstraintsUniqueMessageProjectName implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .on(ConstraintsUniqueMessageProjectCreated, e => e.name) + .withMessage('A project with this name already exists.')); + } +} +``` diff --git a/Documentation/client-snippets/constraints/model-bound/unique-event-type/basic.md b/Documentation/client-snippets/constraints/model-bound/unique-event-type/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/model-bound/unique-event-type/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/model-bound/unique-event-type/name-and-message.md b/Documentation/client-snippets/constraints/model-bound/unique-event-type/name-and-message.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/model-bound/unique-event-type/name-and-message.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/model-bound/unique-event-type/releasing.md b/Documentation/client-snippets/constraints/model-bound/unique-event-type/releasing.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/model-bound/unique-event-type/releasing.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/model-bound/unique/basic.md b/Documentation/client-snippets/constraints/model-bound/unique/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/model-bound/unique/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/model-bound/unique/grouping.md b/Documentation/client-snippets/constraints/model-bound/unique/grouping.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/model-bound/unique/grouping.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/model-bound/unique/multiple-remove-constraints.md b/Documentation/client-snippets/constraints/model-bound/unique/multiple-remove-constraints.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/model-bound/unique/multiple-remove-constraints.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/constraints/model-bound/unique/releasing.md b/Documentation/client-snippets/constraints/model-bound/unique/releasing.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/constraints/model-bound/unique/releasing.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/contributing/clients/typescript-grpc-package/event-stores-definition.md b/Documentation/client-snippets/contributing/clients/typescript-grpc-package/event-stores-definition.md new file mode 100644 index 0000000..af9c156 --- /dev/null +++ b/Documentation/client-snippets/contributing/clients/typescript-grpc-package/event-stores-definition.md @@ -0,0 +1,8 @@ +```typescript +import { EventStoresDefinition } from '@cratis/chronicle.contracts'; + +const eventStoresService = EventStoresDefinition.fullName; +const eventStoresMethods = Object.keys(EventStoresDefinition.methods); + +console.log(`Generated service ${eventStoresService} exposes: ${eventStoresMethods.join(', ')}`); +``` diff --git a/Documentation/client-snippets/contributing/clients/typescript-grpc-package/namespaces-definition.md b/Documentation/client-snippets/contributing/clients/typescript-grpc-package/namespaces-definition.md new file mode 100644 index 0000000..58a79e1 --- /dev/null +++ b/Documentation/client-snippets/contributing/clients/typescript-grpc-package/namespaces-definition.md @@ -0,0 +1,7 @@ +```typescript +import { NamespacesDefinition } from '@cratis/chronicle.contracts'; + +const getNamespacesMethod = NamespacesDefinition.methods.getNamespaces; + +console.log(`${NamespacesDefinition.fullName}.${getNamespacesMethod.name}`); +``` diff --git a/Documentation/client-snippets/contributing/clients/typescript-grpc-package/request-messages.md b/Documentation/client-snippets/contributing/clients/typescript-grpc-package/request-messages.md new file mode 100644 index 0000000..a8bf7aa --- /dev/null +++ b/Documentation/client-snippets/contributing/clients/typescript-grpc-package/request-messages.md @@ -0,0 +1,9 @@ +```typescript +import { EnsureEventStore, EnsureNamespace, GetNamespacesRequest } from '@cratis/chronicle.contracts'; + +const eventStore = EnsureEventStore.create({ Name: 'shopping' }); +const namespace = EnsureNamespace.create({ EventStore: eventStore.Name, Name: 'tenant-one' }); +const namespaces = GetNamespacesRequest.create({ EventStore: eventStore.Name }); + +console.log(eventStore, namespace, namespaces); +``` diff --git a/Documentation/client-snippets/contributing/clients/typescript-grpc-package/service-types.md b/Documentation/client-snippets/contributing/clients/typescript-grpc-package/service-types.md new file mode 100644 index 0000000..50afc42 --- /dev/null +++ b/Documentation/client-snippets/contributing/clients/typescript-grpc-package/service-types.md @@ -0,0 +1,10 @@ +```typescript +import { EventStoresClient, NamespacesClient } from '@cratis/chronicle.contracts'; + +async function readAvailableNamespaces(eventStores: EventStoresClient, namespaces: NamespacesClient): Promise { + await eventStores.ensure({ Name: 'shopping' }); + const result = await namespaces.getNamespaces({ EventStore: 'shopping' }); + + return result.items; +} +``` diff --git a/Documentation/client-snippets/events/appending-many/transfer.md b/Documentation/client-snippets/events/appending-many/transfer.md new file mode 100644 index 0000000..7e5c0dd --- /dev/null +++ b/Documentation/client-snippets/events/appending-many/transfer.md @@ -0,0 +1,30 @@ +```typescript +import { eventType, EventForEventSourceId, IEventStore } from '@cratis/chronicle'; + +@eventType() +class MoneyWithdrawn { + constructor(readonly amount: number) {} +} + +@eventType() +class MoneyDeposited { + constructor(readonly amount: number) {} +} + +class TransferService { + constructor(private readonly store: IEventStore) {} + + async transfer(fromAccount: string, toAccount: string, amount: number): Promise { + const events: EventForEventSourceId[] = [ + { eventSourceId: fromAccount, event: new MoneyWithdrawn(amount) }, + { eventSourceId: toAccount, event: new MoneyDeposited(amount) } + ]; + + const results = await this.store.eventLog.appendMany(events); + + if (results.some(_ => !_.isSuccess)) { + // Decide whether to retry or surface a conflict to the caller. + } + } +} +``` diff --git a/Documentation/client-snippets/events/appending-with-tags/append-many.md b/Documentation/client-snippets/events/appending-with-tags/append-many.md new file mode 100644 index 0000000..7943a87 --- /dev/null +++ b/Documentation/client-snippets/events/appending-with-tags/append-many.md @@ -0,0 +1,4 @@ +```text +The TypeScript Chronicle client does not support this workflow yet. +Its append-many options do not currently expose dynamic event tags. +``` diff --git a/Documentation/client-snippets/events/appending-with-tags/append.md b/Documentation/client-snippets/events/appending-with-tags/append.md new file mode 100644 index 0000000..0515efe --- /dev/null +++ b/Documentation/client-snippets/events/appending-with-tags/append.md @@ -0,0 +1,4 @@ +```text +The TypeScript Chronicle client does not support this workflow yet. +Its append options do not currently expose dynamic event tags. +``` diff --git a/Documentation/client-snippets/events/appending/example.md b/Documentation/client-snippets/events/appending/example.md new file mode 100644 index 0000000..2a781cd --- /dev/null +++ b/Documentation/client-snippets/events/appending/example.md @@ -0,0 +1,26 @@ +```typescript +import { eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class OrderPlaced { + constructor( + readonly customerId: string, + readonly total: number + ) {} +} + +class CheckoutService { + constructor(private readonly store: IEventStore) {} + + async placeOrder(orderId: string, customerId: string, total: number): Promise { + const result = await this.store.eventLog.append( + orderId, + new OrderPlaced(customerId, total) + ); + + if (!result.isSuccess) { + // Decide whether to retry or surface a conflict to the caller. + } + } +} +``` diff --git a/Documentation/client-snippets/events/appending/schema-validation.md b/Documentation/client-snippets/events/appending/schema-validation.md new file mode 100644 index 0000000..f21e4ab --- /dev/null +++ b/Documentation/client-snippets/events/appending/schema-validation.md @@ -0,0 +1,12 @@ +```typescript +const result = await store.eventLog.append( + eventSourceId, + new OrderPlaced(customerId, total) +); + +if (!result.isSuccess) { + for (const error of result.errors) { + console.log(`Schema error: ${error.message}`); + } +} +``` diff --git a/Documentation/client-snippets/events/concurrency/basic.md b/Documentation/client-snippets/events/concurrency/basic.md new file mode 100644 index 0000000..7a28d99 --- /dev/null +++ b/Documentation/client-snippets/events/concurrency/basic.md @@ -0,0 +1,21 @@ +```typescript +import { eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class ConcurrencyAccountOpened { + constructor(readonly accountName: string) {} +} + +class ConcurrencyBankAccountService { + constructor(private readonly store: IEventStore) {} + + async openAccount(accountId: string, accountName: string): Promise { + await this.store.eventLog.append(accountId, new ConcurrencyAccountOpened(accountName), { + concurrencyScope: { + sequenceNumber: 42n, + eventSourceId: true + } + }); + } +} +``` diff --git a/Documentation/client-snippets/events/getting-events/for-event-source.md b/Documentation/client-snippets/events/getting-events/for-event-source.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/events/getting-events/for-event-source.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/events/getting-state/tail.md b/Documentation/client-snippets/events/getting-state/tail.md new file mode 100644 index 0000000..7671c45 --- /dev/null +++ b/Documentation/client-snippets/events/getting-state/tail.md @@ -0,0 +1,12 @@ +```typescript +import { EventSequenceNumber, IEventStore } from '@cratis/chronicle'; + +class GettingStateCheckpointStore { + constructor(private readonly store: IEventStore) {} + + async captureTail(): Promise { + // Persists the current tail so processing can resume later. + return await this.store.eventLog.getTailSequenceNumber(); + } +} +``` diff --git a/Documentation/client-snippets/events/index/event-log.md b/Documentation/client-snippets/events/index/event-log.md new file mode 100644 index 0000000..666778b --- /dev/null +++ b/Documentation/client-snippets/events/index/event-log.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class EventsIndexEmployeesService { + constructor(private readonly store: IEventStore) {} + + async registerEmployee(employeeId: string, firstName: string, lastName: string): Promise { + await this.store.eventLog.append(employeeId, new EventsIndexEmployeeRegistered(firstName, lastName)); + } +} +``` diff --git a/Documentation/client-snippets/events/index/event-type.md b/Documentation/client-snippets/events/index/event-type.md new file mode 100644 index 0000000..511fe7e --- /dev/null +++ b/Documentation/client-snippets/events/index/event-type.md @@ -0,0 +1,11 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class EventsIndexEmployeeRegistered { + constructor( + readonly firstName: string, + readonly lastName: string + ) {} +} +``` diff --git a/Documentation/client-snippets/events/transactions/basic.md b/Documentation/client-snippets/events/transactions/basic.md new file mode 100644 index 0000000..080c7cb --- /dev/null +++ b/Documentation/client-snippets/events/transactions/basic.md @@ -0,0 +1,34 @@ +```typescript +import { IEventStore, eventType } from '@cratis/chronicle'; + +@eventType() +class TransactionalOrderPlaced { + constructor(readonly orderId: string, readonly totalAmount: number) {} +} + +@eventType() +class TransactionalInventoryReserved { + constructor(readonly sku: string, readonly quantity: number) {} +} + +async function commitOrder(store: IEventStore): Promise { + const unitOfWork = store.unitOfWorkManager.begin(); + + try { + await store.eventLog.transactional.append( + 'order-123', + new TransactionalOrderPlaced('order-123', 99.95) + ); + + await store.eventLog.transactional.append( + 'inventory-widget', + new TransactionalInventoryReserved('widget', 1) + ); + + await unitOfWork.commit(); + } catch (error) { + await unitOfWork.rollback(); + throw error; + } +} +``` diff --git a/Documentation/client-snippets/get-started/client-flow.md b/Documentation/client-snippets/get-started/client-flow.md new file mode 100644 index 0000000..c98d7ff --- /dev/null +++ b/Documentation/client-snippets/get-started/client-flow.md @@ -0,0 +1,9 @@ +```typescript title="index.ts" +import 'reflect-metadata'; +import { ChronicleClient, ChronicleOptions } from '@cratis/chronicle'; + +const client = new ChronicleClient(ChronicleOptions.development()); +const eventStore = await client.getEventStore('ChronicleConsole'); + +await eventStore.eventLog.append('some-event-source', new TestEvent('Hello world!')); +``` diff --git a/Documentation/client-snippets/get-started/common/a-events.md b/Documentation/client-snippets/get-started/common/a-events.md new file mode 100644 index 0000000..4d04b14 --- /dev/null +++ b/Documentation/client-snippets/get-started/common/a-events.md @@ -0,0 +1,20 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class GetStartedBookAdded { + constructor( + readonly title: string, + readonly isbn: string + ) {} +} + +@eventType() +class GetStartedBookBorrowed { + constructor(readonly memberName: string) {} +} + +@eventType() +class GetStartedBookReturned { +} +``` diff --git a/Documentation/client-snippets/get-started/common/append.md b/Documentation/client-snippets/get-started/common/append.md new file mode 100644 index 0000000..0cb5155 --- /dev/null +++ b/Documentation/client-snippets/get-started/common/append.md @@ -0,0 +1,13 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class GetStartedBookService { + constructor(private readonly store: IEventStore) {} + + async addBook(bookId: string): Promise { + const eventLog = this.store.eventLog; + + await eventLog.append(bookId, new GetStartedBookAdded('The Pragmatic Programmer', '978-0135957059')); + } +} +``` diff --git a/Documentation/client-snippets/get-started/common/book-read-model.md b/Documentation/client-snippets/get-started/common/book-read-model.md new file mode 100644 index 0000000..25b5b4f --- /dev/null +++ b/Documentation/client-snippets/get-started/common/book-read-model.md @@ -0,0 +1,23 @@ +```typescript +import { fromEvent, Guid, readModel, setFrom, setValue } from '@cratis/chronicle'; + +@readModel() +@fromEvent(GetStartedBookAdded) +class GetStartedBook { + id: Guid = Guid.empty; + + @setFrom(GetStartedBookAdded, 'title') + title = ''; + + @setFrom(GetStartedBookAdded, 'isbn') + isbn = ''; + + @setValue(GetStartedBookAdded, false) + @setValue(GetStartedBookBorrowed, true) + @setValue(GetStartedBookReturned, false) + onLoan = false; + + @setFrom(GetStartedBookBorrowed, 'memberName') + borrowedBy: string | null = null; +} +``` diff --git a/Documentation/client-snippets/get-started/common/borrowed-book-read-model.md b/Documentation/client-snippets/get-started/common/borrowed-book-read-model.md new file mode 100644 index 0000000..88d94b0 --- /dev/null +++ b/Documentation/client-snippets/get-started/common/borrowed-book-read-model.md @@ -0,0 +1,13 @@ +```typescript +import { fromEvent, Guid, readModel, removedWith, setFrom } from '@cratis/chronicle'; + +@readModel() +@fromEvent(GetStartedBookBorrowed) +@removedWith(GetStartedBookReturned) +class GetStartedBorrowedBook { + id: Guid = Guid.empty; + + @setFrom(GetStartedBookBorrowed, 'memberName') + memberName = ''; +} +``` diff --git a/Documentation/client-snippets/get-started/common/materialized-paging.md b/Documentation/client-snippets/get-started/common/materialized-paging.md new file mode 100644 index 0000000..bac1a62 --- /dev/null +++ b/Documentation/client-snippets/get-started/common/materialized-paging.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class GetStartedBookPagingService { + constructor(private readonly store: IEventStore) {} + + getPage(): Promise { + return this.store.readModels.materialized.getInstances(GetStartedBook, 0, 20); + } +} +``` diff --git a/Documentation/client-snippets/get-started/common/query-read-models.md b/Documentation/client-snippets/get-started/common/query-read-models.md new file mode 100644 index 0000000..c192220 --- /dev/null +++ b/Documentation/client-snippets/get-started/common/query-read-models.md @@ -0,0 +1,14 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class GetStartedBookQueryService { + constructor(private readonly store: IEventStore) {} + + async queryBooks(): Promise<{ books: GetStartedBook[]; borrowedBooks: GetStartedBorrowedBook[] }> { + const books = await this.store.readModels.getInstances(GetStartedBook); + const borrowedBooks = await this.store.readModels.getInstances(GetStartedBorrowedBook); + + return { books, borrowedBooks }; + } +} +``` diff --git a/Documentation/client-snippets/get-started/common/reactor.md b/Documentation/client-snippets/get-started/common/reactor.md new file mode 100644 index 0000000..15654ef --- /dev/null +++ b/Documentation/client-snippets/get-started/common/reactor.md @@ -0,0 +1,13 @@ +```typescript +import { EventContext, reactor } from '@cratis/chronicle'; + +@reactor() +class GetStartedBookReturnedNotifier { + // Method name must be the exact camelCase of the event's class name - + // Chronicle discovers handlers by name, not by parameter type. + async getStartedBookReturned(event: GetStartedBookReturned, context: EventContext): Promise { + // context.eventSourceId is the bookId this happened to + console.log(`Book ${context.eventSourceId} was returned — notify the next member in line.`); + } +} +``` diff --git a/Documentation/client-snippets/get-started/console/connect.md b/Documentation/client-snippets/get-started/console/connect.md new file mode 100644 index 0000000..d243918 --- /dev/null +++ b/Documentation/client-snippets/get-started/console/connect.md @@ -0,0 +1,12 @@ +```typescript +import { ChronicleClient, ChronicleOptions } from '@cratis/chronicle'; + +async function run() { + // ChronicleOptions.development() points at the local dev kernel on chronicle://localhost:35000 + const client = new ChronicleClient(ChronicleOptions.development()); + const eventStore = await client.getEventStore('Quickstart'); + console.log(`Connected to event store: ${eventStore.name}`); + + // Use eventStore for the lifetime of your program — appending, querying, and so on. +} +``` diff --git a/Documentation/client-snippets/get-started/test-event.md b/Documentation/client-snippets/get-started/test-event.md new file mode 100644 index 0000000..0fa89e8 --- /dev/null +++ b/Documentation/client-snippets/get-started/test-event.md @@ -0,0 +1,8 @@ +```typescript title="The event - an immutable fact" +import { eventType } from '@cratis/chronicle'; + +@eventType() +class TestEvent { + constructor(readonly message: string) {} +} +``` diff --git a/Documentation/client-snippets/get-started/test-projection.md b/Documentation/client-snippets/get-started/test-projection.md new file mode 100644 index 0000000..add36c0 --- /dev/null +++ b/Documentation/client-snippets/get-started/test-projection.md @@ -0,0 +1,9 @@ +```typescript title="The projection - builds queryable state" +import { fromEvent, readModel } from '@cratis/chronicle'; + +@readModel() +@fromEvent(TestEvent) +class TestProjection { + message = ''; +} +``` diff --git a/Documentation/client-snippets/get-started/test-reactor.md b/Documentation/client-snippets/get-started/test-reactor.md new file mode 100644 index 0000000..511985b --- /dev/null +++ b/Documentation/client-snippets/get-started/test-reactor.md @@ -0,0 +1,10 @@ +```typescript title="The reactor - does something when it happens" +import { reactor } from '@cratis/chronicle'; + +@reactor() +class TestReactor { + async testEvent(event: TestEvent): Promise { + console.log(`Received event with message: ${event.message}`); + } +} +``` diff --git a/Documentation/client-snippets/jobs/index/get-one.md b/Documentation/client-snippets/jobs/index/get-one.md new file mode 100644 index 0000000..8e9df11 --- /dev/null +++ b/Documentation/client-snippets/jobs/index/get-one.md @@ -0,0 +1,12 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; +import { Job } from '@cratis/chronicle.contracts'; + +class JobsIndexGetOne { + constructor(private readonly store: IEventStore) {} + + async getJob(jobId: string): Promise { + return this.store.jobs.getJob(jobId); + } +} +``` diff --git a/Documentation/client-snippets/jobs/index/get-steps.md b/Documentation/client-snippets/jobs/index/get-steps.md new file mode 100644 index 0000000..e779605 --- /dev/null +++ b/Documentation/client-snippets/jobs/index/get-steps.md @@ -0,0 +1,12 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; +import { JobStep } from '@cratis/chronicle.contracts'; + +class JobsIndexGetSteps { + constructor(private readonly store: IEventStore) {} + + async getSteps(jobId: string): Promise { + return this.store.jobs.getJobSteps(jobId); + } +} +``` diff --git a/Documentation/client-snippets/jobs/index/list-all.md b/Documentation/client-snippets/jobs/index/list-all.md new file mode 100644 index 0000000..eb22882 --- /dev/null +++ b/Documentation/client-snippets/jobs/index/list-all.md @@ -0,0 +1,12 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; +import { Job } from '@cratis/chronicle.contracts'; + +class JobsIndexListAll { + constructor(private readonly store: IEventStore) {} + + async getAllJobs(): Promise { + return this.store.jobs.getJobs(); + } +} +``` diff --git a/Documentation/client-snippets/jobs/index/stop-resume-delete.md b/Documentation/client-snippets/jobs/index/stop-resume-delete.md new file mode 100644 index 0000000..627bb93 --- /dev/null +++ b/Documentation/client-snippets/jobs/index/stop-resume-delete.md @@ -0,0 +1,19 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class JobsIndexStopResumeDelete { + constructor(private readonly store: IEventStore) {} + + async stopJob(jobId: string): Promise { + await this.store.jobs.stop(jobId); + } + + async resumeJob(jobId: string): Promise { + await this.store.jobs.resume(jobId); + } + + async deleteJob(jobId: string): Promise { + await this.store.jobs.delete(jobId); + } +} +``` diff --git a/Documentation/client-snippets/projections/architecture/declarative.md b/Documentation/client-snippets/projections/architecture/declarative.md new file mode 100644 index 0000000..fb4ca88 --- /dev/null +++ b/Documentation/client-snippets/projections/architecture/declarative.md @@ -0,0 +1,6 @@ +```text +TypeScript does not support this workflow yet. +The declarative projection builder declares a `.count()` method, but it +currently throws "count is not implemented yet." at runtime. +Track the client SDK issue before relying on declarative counting from TypeScript. +``` diff --git a/Documentation/client-snippets/projections/architecture/model-bound.md b/Documentation/client-snippets/projections/architecture/model-bound.md new file mode 100644 index 0000000..17bc6a1 --- /dev/null +++ b/Documentation/client-snippets/projections/architecture/model-bound.md @@ -0,0 +1,17 @@ +```typescript +import { count, eventType, fromEvent, readModel } from '@cratis/chronicle'; + +@eventType() +class ArchitectureModelBoundItemAdded { + category = ''; +} + +@readModel() +@fromEvent(ArchitectureModelBoundItemAdded, { key: 'category' }) +class ArchitectureModelBoundSummary { + category = ''; + + @count(ArchitectureModelBoundItemAdded) + count = 0; +} +``` diff --git a/Documentation/client-snippets/projections/choosing-a-read-model-style/events.md b/Documentation/client-snippets/projections/choosing-a-read-model-style/events.md new file mode 100644 index 0000000..2841d9a --- /dev/null +++ b/Documentation/client-snippets/projections/choosing-a-read-model-style/events.md @@ -0,0 +1,18 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class ChoosingStyleBookRegistered { + title = ''; + isbn = ''; +} + +@eventType() +class ChoosingStyleBookBorrowed { + memberName = ''; +} + +@eventType() +class ChoosingStyleBookReturned { +} +``` diff --git a/Documentation/client-snippets/projections/choosing-a-read-model-style/fluent.md b/Documentation/client-snippets/projections/choosing-a-read-model-style/fluent.md new file mode 100644 index 0000000..2312531 --- /dev/null +++ b/Documentation/client-snippets/projections/choosing-a-read-model-style/fluent.md @@ -0,0 +1,30 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +class ChoosingStyleBookStatusFluent { + id = ''; + title = ''; + isbn = ''; + isBorrowed = false; + borrowedBy: string | null = null; +} + +@projection() +class ChoosingStyleBookStatusProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(ChoosingStyleBookRegistered, _ => _ + .set(m => m.id).toEventSourceId() + .set(m => m.title).to(e => e.title) + .set(m => m.isbn).to(e => e.isbn) + .set(m => m.isBorrowed).toValue(false) + .set(m => m.borrowedBy).toValue(null)) + .from(ChoosingStyleBookBorrowed, _ => _ + .set(m => m.isBorrowed).toValue(true) + .set(m => m.borrowedBy).to(e => e.memberName)) + .from(ChoosingStyleBookReturned, _ => _ + .set(m => m.isBorrowed).toValue(false) + .set(m => m.borrowedBy).toValue(null)); + } +} +``` diff --git a/Documentation/client-snippets/projections/choosing-a-read-model-style/model-bound.md b/Documentation/client-snippets/projections/choosing-a-read-model-style/model-bound.md new file mode 100644 index 0000000..a24c059 --- /dev/null +++ b/Documentation/client-snippets/projections/choosing-a-read-model-style/model-bound.md @@ -0,0 +1,22 @@ +```typescript +import { readModel, setFrom, setValue } from '@cratis/chronicle'; + +@readModel() +class ChoosingStyleBookStatusModelBound { + id = ''; + + @setFrom(ChoosingStyleBookRegistered, 'title') + title = ''; + + @setFrom(ChoosingStyleBookRegistered, 'isbn') + isbn = ''; + + @setValue(ChoosingStyleBookBorrowed, true) + @setValue(ChoosingStyleBookReturned, false) + isBorrowed = false; + + @setFrom(ChoosingStyleBookBorrowed, 'memberName') + @setValue(ChoosingStyleBookReturned, null) + borrowedBy: string | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/choosing-a-read-model-style/read-model.md b/Documentation/client-snippets/projections/choosing-a-read-model-style/read-model.md new file mode 100644 index 0000000..4636515 --- /dev/null +++ b/Documentation/client-snippets/projections/choosing-a-read-model-style/read-model.md @@ -0,0 +1,9 @@ +```typescript +class ChoosingStyleBookStatus { + id = ''; + title = ''; + isbn = ''; + isBorrowed = false; + borrowedBy: string | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/choosing-a-read-model-style/reducer.md b/Documentation/client-snippets/projections/choosing-a-read-model-style/reducer.md new file mode 100644 index 0000000..595ecd6 --- /dev/null +++ b/Documentation/client-snippets/projections/choosing-a-read-model-style/reducer.md @@ -0,0 +1,43 @@ +```typescript +import { reducer } from '@cratis/chronicle'; + +class ChoosingStyleBookStatusReducerModel { + title = ''; + isbn = ''; + isBorrowed = false; + borrowedBy: string | null = null; +} + +// Handler methods receive only the event and the current state - there is no +// event-context parameter, unlike the C# reducer's EventContext argument. +// The method name must be the exact camelCase of the event's class name - +// Chronicle discovers handlers by name, not by parameter type. +@reducer('', undefined, ChoosingStyleBookStatusReducerModel) +class ChoosingStyleBookStatusReducer { + choosingStyleBookRegistered( + event: ChoosingStyleBookRegistered, + current: ChoosingStyleBookStatusReducerModel | undefined + ): ChoosingStyleBookStatusReducerModel { + return { + title: event.title, + isbn: event.isbn, + isBorrowed: false, + borrowedBy: null + }; + } + + choosingStyleBookBorrowed( + event: ChoosingStyleBookBorrowed, + current: ChoosingStyleBookStatusReducerModel + ): ChoosingStyleBookStatusReducerModel { + return { ...current, isBorrowed: true, borrowedBy: event.memberName }; + } + + choosingStyleBookReturned( + event: ChoosingStyleBookReturned, + current: ChoosingStyleBookStatusReducerModel + ): ChoosingStyleBookStatusReducerModel { + return { ...current, isBorrowed: false, borrowedBy: null }; + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/auto-map/basic.md b/Documentation/client-snippets/projections/declarative/auto-map/basic.md new file mode 100644 index 0000000..e0794b2 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/auto-map/basic.md @@ -0,0 +1,28 @@ +```typescript title="AutoMap by convention" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class AutoMapUserCreated { + constructor(readonly name: string, readonly email: string) {} +} + +@eventType() +class AutoMapUserRenamed { + constructor(readonly name: string) {} +} + +@readModel() +class AutoMapUser { + name = ''; + email = ''; +} + +@projection('', AutoMapUser) +class AutoMapUserProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(AutoMapUserCreated) + .from(AutoMapUserRenamed); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/auto-map/disable.md b/Documentation/client-snippets/projections/declarative/auto-map/disable.md new file mode 100644 index 0000000..19ce384 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/auto-map/disable.md @@ -0,0 +1,27 @@ +```typescript title="Disable AutoMap" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class AutoMapDisabledAccountRegistered { + constructor(readonly accountName: string, readonly contactEmail: string) {} +} + +@readModel() +class AutoMapDisabledAccount { + name = ''; + email = ''; + createdAt = new Date(0); +} + +@projection('', AutoMapDisabledAccount) +class AutoMapDisabledAccountProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .noAutoMap() + .from(AutoMapDisabledAccountRegistered, _ => _ + .set(m => m.name).to(e => e.accountName) + .set(m => m.email).to(e => e.contactEmail) + .set(m => m.createdAt).toEventContextProperty('occurred')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/auto-map/explicit-mappings.md b/Documentation/client-snippets/projections/declarative/auto-map/explicit-mappings.md new file mode 100644 index 0000000..3859e08 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/auto-map/explicit-mappings.md @@ -0,0 +1,32 @@ +```typescript title="AutoMap with explicit mappings" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class AutoMapAccountOpened { + constructor(readonly name: string, readonly email: string) {} +} + +@eventType() +class AutoMapAccountEmailChanged { + constructor(readonly email: string) {} +} + +@readModel() +class AutoMapAccount { + name = ''; + email = ''; + status = ''; + createdAt = new Date(0); +} + +@projection('', AutoMapAccount) +class AutoMapAccountProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(AutoMapAccountOpened, _ => _ + .set(m => m.status).toValue('Active') + .set(m => m.createdAt).toEventContextProperty('occurred')) + .from(AutoMapAccountEmailChanged); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/auto-map/join.md b/Documentation/client-snippets/projections/declarative/auto-map/join.md new file mode 100644 index 0000000..2c6ee47 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/auto-map/join.md @@ -0,0 +1,30 @@ +```typescript title="AutoMap with a join" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class AutoMapEmployeeHired { + constructor(readonly employeeName: string, readonly departmentId: string) {} +} + +@eventType() +class AutoMapDepartmentRenamed { + constructor(readonly departmentName: string) {} +} + +@readModel() +class AutoMapEmployee { + employeeName = ''; + departmentId = ''; + departmentName = ''; +} + +@projection('', AutoMapEmployee) +class AutoMapEmployeeProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(AutoMapEmployeeHired) + .join(AutoMapDepartmentRenamed, _ => _ + .on(m => m.departmentId)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/composite-keys/audit-model.md b/Documentation/client-snippets/projections/declarative/composite-keys/audit-model.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/composite-keys/audit-model.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/declarative/composite-keys/basic.md b/Documentation/client-snippets/projections/declarative/composite-keys/basic.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/composite-keys/basic.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/declarative/composite-keys/child-collection.md b/Documentation/client-snippets/projections/declarative/composite-keys/child-collection.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/composite-keys/child-collection.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/declarative/composite-keys/context-key.md b/Documentation/client-snippets/projections/declarative/composite-keys/context-key.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/composite-keys/context-key.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/declarative/composite-keys/events.md b/Documentation/client-snippets/projections/declarative/composite-keys/events.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/composite-keys/events.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/declarative/composite-keys/join.md b/Documentation/client-snippets/projections/declarative/composite-keys/join.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/composite-keys/join.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/declarative/composite-keys/key-type.md b/Documentation/client-snippets/projections/declarative/composite-keys/key-type.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/composite-keys/key-type.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/declarative/composite-keys/read-model.md b/Documentation/client-snippets/projections/declarative/composite-keys/read-model.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/composite-keys/read-model.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/declarative/constant-key/increment-decrement.md b/Documentation/client-snippets/projections/declarative/constant-key/increment-decrement.md new file mode 100644 index 0000000..ebbf918 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/constant-key/increment-decrement.md @@ -0,0 +1,28 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecConstantKeyUserLoggedIn { +} + +@eventType() +class DecConstantKeyUserLoggedOut { +} + +class DecConstantKeySiteStatistics { + activeSessions = 0; +} + +@projection() +class DecConstantKeySiteStatisticsProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(DecConstantKeyUserLoggedIn, _ => _ + .usingConstantKey('site') + .increment(m => m.activeSessions)) + .from(DecConstantKeyUserLoggedOut, _ => _ + .usingConstantKey('site') + .decrement(m => m.activeSessions)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/event-context/automap-with-context.md b/Documentation/client-snippets/projections/declarative/event-context/automap-with-context.md new file mode 100644 index 0000000..e65ea12 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/event-context/automap-with-context.md @@ -0,0 +1,21 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecEventContextUserAction { + userId = ''; + actionType = ''; +} + +@projection() +class DecEventContextAuditTrailProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() + .from(DecEventContextUserAction, _ => _ + .set(m => m.eventId).toEventContextProperty('sequenceNumber') + .set(m => m.occurredAt).toEventContextProperty('occurred') + .set(m => m.correlationId).toEventContextProperty('correlationId')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/event-context/basic.md b/Documentation/client-snippets/projections/declarative/event-context/basic.md new file mode 100644 index 0000000..1dc6c9e --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/event-context/basic.md @@ -0,0 +1,16 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@projection() +class DecEventContextUserActivityProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(DecEventContextUserLoggedIn, _ => _ + .set(m => m.userId).toEventSourceId() + .set(m => m.lastLogin).toEventContextProperty('occurred')) + .from(DecEventContextUserPerformedAction, _ => _ + .set(m => m.userId).toEventSourceId() + .set(m => m.lastActivity).toEventContextProperty('occurred')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/event-context/events.md b/Documentation/client-snippets/projections/declarative/event-context/events.md new file mode 100644 index 0000000..67bd2bb --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/event-context/events.md @@ -0,0 +1,14 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class DecEventContextUserLoggedIn { + username = ''; +} + +@eventType() +class DecEventContextUserPerformedAction { + userId = ''; + actionType = ''; +} +``` diff --git a/Documentation/client-snippets/projections/declarative/event-context/read-models.md b/Documentation/client-snippets/projections/declarative/event-context/read-models.md new file mode 100644 index 0000000..0ad2e58 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/event-context/read-models.md @@ -0,0 +1,15 @@ +```typescript +class DecEventContextUserActivity { + userId = ''; + lastLogin = new Date(); + lastActivity = new Date(); +} + +class DecEventContextAuditEntry { + eventId: bigint = 0n; + occurredAt = new Date(); + correlationId = ''; + actionType = ''; + userId = ''; +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-all/basic.md b/Documentation/client-snippets/projections/declarative/from-all/basic.md new file mode 100644 index 0000000..d6ad928 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-all/basic.md @@ -0,0 +1,33 @@ +```typescript title="Declarative FromAll" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class UserCreatedDeclarativeAll { + constructor(readonly name: string, readonly email: string) {} +} + +@eventType() +class UserEmailChangedDeclarativeAll { + constructor(readonly email: string) {} +} + +@readModel() +class UserProfileDeclarativeAll { + name = ''; + email = ''; + lastUpdated = new Date(); +} + +@projection('', UserProfileDeclarativeAll) +class UserProfileDeclarativeAllProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(UserCreatedDeclarativeAll) + .from(UserEmailChangedDeclarativeAll) + .fromEvery(_ => _ + .set(m => m.lastUpdated) + .toEventContextProperty('occurred') + .excludeChildProjections()); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-all/context-fields.md b/Documentation/client-snippets/projections/declarative/from-all/context-fields.md new file mode 100644 index 0000000..54eea6d --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-all/context-fields.md @@ -0,0 +1,28 @@ +```typescript title="Map context fields with FromAll" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class AccountTouchedDeclarativeAll { + constructor(readonly reason: string) {} +} + +@readModel() +class AccountAuditDeclarativeAll { + lastUpdated = new Date(); + lastEventSequence = 0n; + lastCorrelationId = ''; +} + +@projection('', AccountAuditDeclarativeAll) +class AccountAuditDeclarativeAllProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(AccountTouchedDeclarativeAll) + .fromEvery(_ => _ + .set(m => m.lastUpdated).toEventContextProperty('occurred') + .set(m => m.lastEventSequence).toEventContextProperty('sequenceNumber') + .set(m => m.lastCorrelationId).toEventContextProperty('correlationId') + .excludeChildProjections()); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-all/event-source-id.md b/Documentation/client-snippets/projections/declarative/from-all/event-source-id.md new file mode 100644 index 0000000..6c0d501 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-all/event-source-id.md @@ -0,0 +1,26 @@ +```typescript title="Map event source id with FromAll" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class AccountOpenedDeclarativeAll { + constructor(readonly ownerName: string) {} +} + +@readModel() +class AccountSummaryDeclarativeAll { + accountId = ''; + ownerName = ''; +} + +@projection('', AccountSummaryDeclarativeAll) +class AccountSummaryDeclarativeAllProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(AccountOpenedDeclarativeAll) + .fromEvery(_ => _ + .set(m => m.accountId) + .toEventSourceId() + .excludeChildProjections()); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-all/with-specific-mappings.md b/Documentation/client-snippets/projections/declarative/from-all/with-specific-mappings.md new file mode 100644 index 0000000..d31fbc4 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-all/with-specific-mappings.md @@ -0,0 +1,37 @@ +```typescript title="Combine FromAll with event-specific mappings" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class OrderCreatedDeclarativeAll { + constructor(readonly orderNumber: string) {} +} + +@eventType() +class OrderShippedDeclarativeAll { + constructor(readonly trackingNumber: string) {} +} + +@readModel() +class OrderDeclarativeAll { + orderNumber = ''; + status = ''; + lastModified = new Date(); +} + +@projection('', OrderDeclarativeAll) +class OrderDeclarativeAllProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .fromEvery(_ => _ + .set(m => m.lastModified) + .toEventContextProperty('occurred') + .excludeChildProjections()) + .from(OrderCreatedDeclarativeAll, _ => _ + .set(m => m.status) + .toValue('Placed')) + .from(OrderShippedDeclarativeAll, _ => _ + .set(m => m.status) + .toValue('Shipped')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-event-sequence/basic.md b/Documentation/client-snippets/projections/declarative/from-event-sequence/basic.md new file mode 100644 index 0000000..8ce44e2 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-event-sequence/basic.md @@ -0,0 +1,15 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@projection() +class DecFromEventSequenceOrderProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .fromEventSequence('order-management') + .autoMap() + .from(DecFromEventSequenceOrderCreated) + .from(DecFromEventSequenceOrderUpdated) + .from(DecFromEventSequenceOrderShipped); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-event-sequence/events.md b/Documentation/client-snippets/projections/declarative/from-event-sequence/events.md new file mode 100644 index 0000000..b3c75b4 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-event-sequence/events.md @@ -0,0 +1,22 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class DecFromEventSequenceOrderCreated { + orderNumber = ''; + customerId = ''; + totalAmount = 0; +} + +@eventType() +class DecFromEventSequenceOrderUpdated { + orderNumber = ''; + newTotalAmount = 0; +} + +@eventType() +class DecFromEventSequenceOrderShipped { + orderNumber = ''; + shippedAt = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-event-sequence/multiple-sequences.md b/Documentation/client-snippets/projections/declarative/from-event-sequence/multiple-sequences.md new file mode 100644 index 0000000..d03ceea --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-event-sequence/multiple-sequences.md @@ -0,0 +1,51 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecFromEventSequencePackageCreated { + packageId = ''; +} + +@eventType() +class DecFromEventSequencePackageShipped { + packageId = ''; + shippedAt = new Date(); +} + +@eventType() +class DecFromEventSequencePackageDelivered { + packageId = ''; + deliveredAt = new Date(); +} + +class DecFromEventSequenceShipping { + packageId = ''; + shippedAt: Date | null = null; + deliveredAt: Date | null = null; +} + +// Projection for order management events +@projection() +class DecFromEventSequenceMultiOrderProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .fromEventSequence('order-management') + .autoMap() + .from(DecFromEventSequenceOrderCreated, _ => _ + .set(m => m.status).toValue(DecFromEventSequenceOrderStatus.Created)); + } +} + +// Projection for shipping events from a different sequence +@projection() +class DecFromEventSequenceShippingProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .fromEventSequence('shipping-management') + .autoMap() + .from(DecFromEventSequencePackageCreated) + .from(DecFromEventSequencePackageShipped) + .from(DecFromEventSequencePackageDelivered); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-event-sequence/read-model.md b/Documentation/client-snippets/projections/declarative/from-event-sequence/read-model.md new file mode 100644 index 0000000..9ef36ee --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-event-sequence/read-model.md @@ -0,0 +1,18 @@ +```typescript +enum DecFromEventSequenceOrderStatus { + Created = 'Created', + Processing = 'Processing', + Shipped = 'Shipped', + Delivered = 'Delivered', + Cancelled = 'Cancelled' +} + +class DecFromEventSequenceOrder { + orderNumber = ''; + customerId = ''; + totalAmount = 0; + status = DecFromEventSequenceOrderStatus.Created; + createdAt = new Date(); + shippedAt: Date | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-event-sequence/using-constant.md b/Documentation/client-snippets/projections/declarative/from-event-sequence/using-constant.md new file mode 100644 index 0000000..0b07763 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-event-sequence/using-constant.md @@ -0,0 +1,19 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +const eventSequences = { + orderManagement: 'order-management' +}; + +@projection() +class DecFromEventSequenceOrderProjectionWithConstant implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + // Using a constant instead of a raw string keeps the sequence identifier consistent + // wherever it is referenced. + .fromEventSequence(eventSequences.orderManagement) + .autoMap() + .from(DecFromEventSequenceOrderCreated); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-every/basic.md b/Documentation/client-snippets/projections/declarative/from-every/basic.md new file mode 100644 index 0000000..afaf1c0 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-every/basic.md @@ -0,0 +1,32 @@ +```typescript title="Declarative fromEvery" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class UserCreatedDeclarativeEvery { + constructor(readonly name: string, readonly email: string) {} +} + +@eventType() +class UserEmailChangedDeclarativeEvery { + constructor(readonly email: string) {} +} + +@readModel() +class UserProfileDeclarativeEvery { + name = ''; + email = ''; + lastUpdated = new Date(); +} + +@projection('', UserProfileDeclarativeEvery) +class UserProfileDeclarativeEveryProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(UserCreatedDeclarativeEvery) + .from(UserEmailChangedDeclarativeEvery) + .fromEvery(_ => _ + .set(m => m.lastUpdated) + .toEventContextProperty('occurred')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-every/context-fields.md b/Documentation/client-snippets/projections/declarative/from-every/context-fields.md new file mode 100644 index 0000000..62e9904 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-every/context-fields.md @@ -0,0 +1,27 @@ +```typescript title="Map multiple context fields" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class AccountTouchedDeclarativeEvery { + constructor(readonly reason: string) {} +} + +@readModel() +class AccountAuditDeclarativeEvery { + lastUpdated = new Date(); + lastEventSequence = 0n; + lastCorrelationId = ''; +} + +@projection('', AccountAuditDeclarativeEvery) +class AccountAuditDeclarativeEveryProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(AccountTouchedDeclarativeEvery) + .fromEvery(_ => _ + .set(m => m.lastUpdated).toEventContextProperty('occurred') + .set(m => m.lastEventSequence).toEventContextProperty('sequenceNumber') + .set(m => m.lastCorrelationId).toEventContextProperty('correlationId')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-every/event-source-id.md b/Documentation/client-snippets/projections/declarative/from-every/event-source-id.md new file mode 100644 index 0000000..177affb --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-every/event-source-id.md @@ -0,0 +1,25 @@ +```typescript title="Map the event source id" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class AccountOpenedDeclarativeEvery { + constructor(readonly ownerName: string) {} +} + +@readModel() +class AccountSummaryDeclarativeEvery { + accountId = ''; + ownerName = ''; +} + +@projection('', AccountSummaryDeclarativeEvery) +class AccountSummaryDeclarativeEveryProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(AccountOpenedDeclarativeEvery) + .fromEvery(_ => _ + .set(m => m.accountId) + .toEventSourceId()); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-every/exclude-children.md b/Documentation/client-snippets/projections/declarative/from-every/exclude-children.md new file mode 100644 index 0000000..a6c258a --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-every/exclude-children.md @@ -0,0 +1,26 @@ +```typescript title="Exclude child projection events" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class OrderCreatedDeclarativeEveryExclude { + constructor(readonly orderNumber: string) {} +} + +@readModel() +class OrderAuditDeclarativeEveryExclude { + orderNumber = ''; + lastUpdated = new Date(); +} + +@projection('', OrderAuditDeclarativeEveryExclude) +class OrderAuditDeclarativeEveryExcludeProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(OrderCreatedDeclarativeEveryExclude) + .fromEvery(_ => _ + .set(m => m.lastUpdated) + .toEventContextProperty('occurred') + .excludeChildProjections()); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/from-every/multiple-declarations.md b/Documentation/client-snippets/projections/declarative/from-every/multiple-declarations.md new file mode 100644 index 0000000..0b1438c --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/from-every/multiple-declarations.md @@ -0,0 +1,29 @@ +```typescript title="Multiple fromEvery declarations" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class UserChangedDeclarativeEveryMultiple { + constructor(readonly name: string) {} +} + +@readModel() +class UserAuditDeclarativeEveryMultiple { + name = ''; + lastUpdated = new Date(); + modifiedBy = ''; +} + +@projection('', UserAuditDeclarativeEveryMultiple) +class UserAuditDeclarativeEveryMultipleProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(UserChangedDeclarativeEveryMultiple) + .fromEvery(_ => _ + .set(m => m.lastUpdated) + .toEventContextProperty('occurred')) + .fromEvery(_ => _ + .set(m => m.modifiedBy) + .toEventContextProperty('causedBy')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/functions/increment-decrement.md b/Documentation/client-snippets/projections/declarative/functions/increment-decrement.md new file mode 100644 index 0000000..b05154a --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/functions/increment-decrement.md @@ -0,0 +1,29 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecFunctionsItemAdded { + name = ''; +} + +@eventType() +class DecFunctionsItemRemoved { + name = ''; +} + +class DecFunctionsInventory { + quantity = 0; +} + +@projection() +class DecFunctionsInventoryProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() + .from(DecFunctionsItemAdded, _ => _ + .increment(m => m.quantity)) + .from(DecFunctionsItemRemoved, _ => _ + .decrement(m => m.quantity)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/index/basic.md b/Documentation/client-snippets/projections/declarative/index/basic.md new file mode 100644 index 0000000..7622b82 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/index/basic.md @@ -0,0 +1,23 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecIndexUserRegistered { + name = ''; + email = ''; + registeredAt = new Date(); +} + +class DecIndexUserProfile { + name = ''; + email = ''; + registeredAt = new Date(); +} + +@projection() +class DecIndexUserProfileProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder.from(DecIndexUserRegistered); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/initial-values/basic.md b/Documentation/client-snippets/projections/declarative/initial-values/basic.md new file mode 100644 index 0000000..9f252d2 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/initial-values/basic.md @@ -0,0 +1,35 @@ +```typescript title="Initial values" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +enum InitialValuesUserStatus { + Inactive = 'Inactive', + Active = 'Active' +} + +@eventType() +class InitialValuesUserCreated { + constructor(readonly name: string, readonly email: string) {} +} + +@readModel() +class InitialValuesUserProfile { + name = 'Unknown user'; + email = ''; + status = InitialValuesUserStatus.Inactive; + createdAt = new Date(0); + lastLogin: Date | null = null; + loginCount = 0; + isVerified = false; +} + +@projection('', InitialValuesUserProfile) +class InitialValuesUserProfileProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .withInitialValues(() => new InitialValuesUserProfile()) + .from(InitialValuesUserCreated, _ => _ + .set(m => m.status).toValue(InitialValuesUserStatus.Active) + .set(m => m.createdAt).toEventContextProperty('occurred')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/initial-values/business-defaults.md b/Documentation/client-snippets/projections/declarative/initial-values/business-defaults.md new file mode 100644 index 0000000..23a3641 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/initial-values/business-defaults.md @@ -0,0 +1,33 @@ +```typescript title="Business defaults" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +enum InitialValuesOrderStatus { + Draft = 'Draft', + Submitted = 'Submitted' +} + +@eventType() +class InitialValuesOrderSubmitted { + constructor(readonly customerName: string, readonly totalAmount: number) {} +} + +@readModel() +class InitialValuesOrderSummary { + customerName = ''; + status = InitialValuesOrderStatus.Draft; + totalAmount = 0; + submittedAt = new Date(0); + notes = 'No notes'; +} + +@projection('', InitialValuesOrderSummary) +class InitialValuesOrderSummaryProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .withInitialValues(() => new InitialValuesOrderSummary()) + .from(InitialValuesOrderSubmitted, _ => _ + .set(m => m.status).toValue(InitialValuesOrderStatus.Submitted) + .set(m => m.submittedAt).toEventContextProperty('occurred')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/initial-values/collections.md b/Documentation/client-snippets/projections/declarative/initial-values/collections.md new file mode 100644 index 0000000..da575bb --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/initial-values/collections.md @@ -0,0 +1,29 @@ +```typescript title="Initialize collections" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +class InitialValuesAddress { + street = ''; + city = ''; +} + +@eventType() +class InitialValuesCustomerRegistered { + constructor(readonly name: string) {} +} + +@readModel() +class InitialValuesCustomerRecord { + name = ''; + addresses: InitialValuesAddress[] = []; + tags: string[] = []; +} + +@projection('', InitialValuesCustomerRecord) +class InitialValuesCustomerRecordProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .withInitialValues(() => new InitialValuesCustomerRecord()) + .from(InitialValuesCustomerRegistered); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/initial-values/partial-events.md b/Documentation/client-snippets/projections/declarative/initial-values/partial-events.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/initial-values/partial-events.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/projections/declarative/joins/basic.md b/Documentation/client-snippets/projections/declarative/joins/basic.md new file mode 100644 index 0000000..de7b505 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/joins/basic.md @@ -0,0 +1,42 @@ +```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 = ''; +} + +@projection() +class DecJoinsUserProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() + .from(DecJoinsUserCreated) + .from(DecJoinsUserAssignedToGroup, b => b + .usingKey(e => e.userId) + .set(m => m.groupId).toEventSourceId()) + .join(DecJoinsGroupCreated, j => j + .on(m => m.groupId)) + .join(DecJoinsGroupRenamed, j => j + .on(m => m.groupId)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/joins/multiple-joins.md b/Documentation/client-snippets/projections/declarative/joins/multiple-joins.md new file mode 100644 index 0000000..25601d7 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/joins/multiple-joins.md @@ -0,0 +1,46 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecJoinsMultipleEmployeeAssigned { + groupId = ''; + departmentId = ''; + locationId = ''; +} + +@eventType() +class DecJoinsMultipleGroupCreated { + name = ''; +} + +@eventType() +class DecJoinsMultipleDepartmentCreated { + name = ''; +} + +@eventType() +class DecJoinsMultipleLocationUpdated { + address = ''; +} + +class DecJoinsMultipleEmployeeSummary { + groupId: string | null = null; + groupName: string | null = null; + departmentId: string | null = null; + departmentName: string | null = null; + locationId: string | null = null; + locationAddress: string | null = null; +} + +@projection() +class DecJoinsMultipleEmployeeSummaryProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() + .from(DecJoinsMultipleEmployeeAssigned) + .join(DecJoinsMultipleGroupCreated, j => j.on(m => m.groupId)) + .join(DecJoinsMultipleDepartmentCreated, j => j.on(m => m.departmentId)) + .join(DecJoinsMultipleLocationUpdated, j => j.on(m => m.locationId)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/joins/read-model.md b/Documentation/client-snippets/projections/declarative/joins/read-model.md new file mode 100644 index 0000000..c612233 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/joins/read-model.md @@ -0,0 +1,9 @@ +```typescript +class DecJoinsUser { + name = ''; + email = ''; + groupId: string | null = null; + groupName: string | null = null; + groupDescription: string | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/declarative/not-rewindable/audit-use-case.md b/Documentation/client-snippets/projections/declarative/not-rewindable/audit-use-case.md new file mode 100644 index 0000000..e8cc827 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/not-rewindable/audit-use-case.md @@ -0,0 +1,34 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecNotRewindableUserLoginAttempt { + userId = ''; + succeeded = false; +} + +@eventType() +class DecNotRewindablePermissionChange { + userId = ''; + permission = ''; +} + +class DecNotRewindableSecurityAuditEntry { + auditedAt = new Date(); + sequenceNumber: bigint = 0n; +} + +@projection() +class DecNotRewindableSecurityAuditProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .notRewindable() + .autoMap() + .fromEvery(_ => _ + .set(m => m.auditedAt).toEventContextProperty('occurred') + .set(m => m.sequenceNumber).toEventContextProperty('sequenceNumber')) + .from(DecNotRewindableUserLoginAttempt) + .from(DecNotRewindablePermissionChange); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/not-rewindable/basic.md b/Documentation/client-snippets/projections/declarative/not-rewindable/basic.md new file mode 100644 index 0000000..feacec3 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/not-rewindable/basic.md @@ -0,0 +1,16 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@projection() +class DecNotRewindableAuditLogProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .notRewindable() + .autoMap() + .fromEvery(_ => _ + .set(m => m.processedAt).toEventContextProperty('occurred')) + .from(DecNotRewindableUserAction, _ => _ + .set(m => m.occurredAt).toEventContextProperty('occurred')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/not-rewindable/combined-features.md b/Documentation/client-snippets/projections/declarative/not-rewindable/combined-features.md new file mode 100644 index 0000000..9420524 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/not-rewindable/combined-features.md @@ -0,0 +1,42 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecNotRewindableOrderReceived { + orderId = ''; +} + +@eventType() +class DecNotRewindableOrderProcessing { + orderId = ''; +} + +@eventType() +class DecNotRewindableOrderCompleted { + orderId = ''; +} + +class DecNotRewindableOrderStatus { + status = ''; + lastUpdatedAt = new Date(); +} + +@projection() +class DecNotRewindableRealTimeOrderStatusProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .notRewindable() + .fromEventSequence('order-processing') + .passive() + .autoMap() + .fromEvery(_ => _ + .set(m => m.lastUpdatedAt).toEventContextProperty('occurred')) + .from(DecNotRewindableOrderReceived, _ => _ + .set(m => m.status).toValue('RECEIVED')) + .from(DecNotRewindableOrderProcessing, _ => _ + .set(m => m.status).toValue('PROCESSING')) + .from(DecNotRewindableOrderCompleted, _ => _ + .set(m => m.status).toValue('COMPLETED')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/not-rewindable/events.md b/Documentation/client-snippets/projections/declarative/not-rewindable/events.md new file mode 100644 index 0000000..05f9114 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/not-rewindable/events.md @@ -0,0 +1,17 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class DecNotRewindableUserAction { + userId = ''; + actionType = ''; + details = ''; +} + +@eventType() +class DecNotRewindableSystemEvent { + componentName = ''; + eventType = ''; + data = ''; +} +``` diff --git a/Documentation/client-snippets/projections/declarative/not-rewindable/financial-use-case.md b/Documentation/client-snippets/projections/declarative/not-rewindable/financial-use-case.md new file mode 100644 index 0000000..e8db003 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/not-rewindable/financial-use-case.md @@ -0,0 +1,27 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecNotRewindablePaymentProcessed { + paymentId = ''; + amount = 0; +} + +class DecNotRewindableLedgerEntry { + recordedAt = new Date(); + transactionType = ''; +} + +@projection() +class DecNotRewindableTransactionLedgerProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .notRewindable() + .autoMap() + .fromEvery(_ => _ + .set(m => m.recordedAt).toEventContextProperty('occurred')) + .from(DecNotRewindablePaymentProcessed, _ => _ + .set(m => m.transactionType).toValue('PAYMENT')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/not-rewindable/performance-use-case.md b/Documentation/client-snippets/projections/declarative/not-rewindable/performance-use-case.md new file mode 100644 index 0000000..3ad2a45 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/not-rewindable/performance-use-case.md @@ -0,0 +1,25 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class DecNotRewindableApiRequestCompleted { + endpoint = ''; + statusCode = 0; + durationMilliseconds: bigint = 0n; +} + +class DecNotRewindablePerformanceMetric { + timestamp = new Date(); +} + +@projection() +class DecNotRewindablePerformanceMetricProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .notRewindable() + .autoMap() + .from(DecNotRewindableApiRequestCompleted, _ => _ + .set(m => m.timestamp).toEventContextProperty('occurred')); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/not-rewindable/read-model.md b/Documentation/client-snippets/projections/declarative/not-rewindable/read-model.md new file mode 100644 index 0000000..546d2f7 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/not-rewindable/read-model.md @@ -0,0 +1,10 @@ +```typescript +class DecNotRewindableAuditLogEntry { + userId = ''; + action = ''; + details = ''; + occurredAt = new Date(); + processedAt = new Date(); + sequenceNumber: bigint = 0n; +} +``` diff --git a/Documentation/client-snippets/projections/declarative/passive/events.md b/Documentation/client-snippets/projections/declarative/passive/events.md new file mode 100644 index 0000000..3e673e6 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/passive/events.md @@ -0,0 +1,20 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class DecPassiveUserCreated { + name = ''; + email = ''; +} + +@eventType() +class DecPassiveUserUpdated { + name = ''; + email = ''; +} + +@eventType() +class DecPassiveUserLoggedIn { + loginTime = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/declarative/passive/projection.md b/Documentation/client-snippets/projections/declarative/passive/projection.md new file mode 100644 index 0000000..b2f60f0 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/passive/projection.md @@ -0,0 +1,14 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@projection() +class DecPassiveUserSummaryProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .passive() + .autoMap() + .from(DecPassiveUserCreated) + .from(DecPassiveUserUpdated); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/passive/read-model.md b/Documentation/client-snippets/projections/declarative/passive/read-model.md new file mode 100644 index 0000000..b75b44a --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/passive/read-model.md @@ -0,0 +1,8 @@ +```typescript +class DecPassiveUserSummary { + name = ''; + email = ''; + loginCount = 0; + lastLoginAt = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/declarative/passive/service.md b/Documentation/client-snippets/projections/declarative/passive/service.md new file mode 100644 index 0000000..2408dcb --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/passive/service.md @@ -0,0 +1,12 @@ +```typescript +import { ChronicleClient } from '@cratis/chronicle'; + +class DecPassiveUserService { + constructor(private readonly client: ChronicleClient) {} + + async getUserSummary(userId: string): Promise { + const store = await this.client.getEventStore('MyStore'); + return store.readModels.getInstanceById(DecPassiveUserSummary, userId); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/set-properties/combined-automap.md b/Documentation/client-snippets/projections/declarative/set-properties/combined-automap.md new file mode 100644 index 0000000..c88a4f1 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/set-properties/combined-automap.md @@ -0,0 +1,15 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@projection() +class DecSetPropsCombinedAccountProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .autoMap() // Automatically maps matching properties + .from(DecSetPropsAccountOpened, _ => _ + .set(m => m.customerName).to(e => e.owner.name) // Custom mapping for nested property + .set(m => m.isActive).toValue(true)) // Custom mapping for constant + .from(DecSetPropsMoneyDeposited); // Uses AutoMap for all properties + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/set-properties/events.md b/Documentation/client-snippets/projections/declarative/set-properties/events.md new file mode 100644 index 0000000..3e36f2b --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/set-properties/events.md @@ -0,0 +1,21 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +class DecSetPropsCustomer { + name = ''; + email = ''; +} + +@eventType() +class DecSetPropsAccountOpened { + number = ''; + owner = new DecSetPropsCustomer(); + timestamp = new Date(); +} + +@eventType() +class DecSetPropsMoneyDeposited { + amount = 0; + timestamp = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/declarative/set-properties/explicit.md b/Documentation/client-snippets/projections/declarative/set-properties/explicit.md new file mode 100644 index 0000000..1df947c --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/set-properties/explicit.md @@ -0,0 +1,19 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@projection() +class DecSetPropsAccountProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(DecSetPropsAccountOpened, _ => _ + .set(m => m.accountNumber).to(e => e.number) + .set(m => m.customerName).to(e => e.owner.name) + .set(m => m.balance).toValue(42.0) + .set(m => m.isActive).toValue(true) + .set(m => m.openedAt).to(e => e.timestamp)) + .from(DecSetPropsMoneyDeposited, _ => _ + .set(m => m.balance).to(e => e.amount) + .set(m => m.lastTransaction).to(e => e.timestamp)); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/set-properties/read-model.md b/Documentation/client-snippets/projections/declarative/set-properties/read-model.md new file mode 100644 index 0000000..4167356 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/set-properties/read-model.md @@ -0,0 +1,10 @@ +```typescript +class DecSetPropsAccount { + accountNumber = ''; + customerName = ''; + balance = 0; + isActive = false; + openedAt = new Date(); + lastTransaction: Date | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/declarative/simple-projection/event.md b/Documentation/client-snippets/projections/declarative/simple-projection/event.md new file mode 100644 index 0000000..63c723a --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/simple-projection/event.md @@ -0,0 +1,7 @@ +```typescript +class DecSimpleUserCreated { + name = ''; + email = ''; + createdAt = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/declarative/simple-projection/projection.md b/Documentation/client-snippets/projections/declarative/simple-projection/projection.md new file mode 100644 index 0000000..beba3b1 --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/simple-projection/projection.md @@ -0,0 +1,10 @@ +```typescript +import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@projection() +class DecSimpleUserProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder.from(DecSimpleUserCreated); + } +} +``` diff --git a/Documentation/client-snippets/projections/declarative/simple-projection/read-model.md b/Documentation/client-snippets/projections/declarative/simple-projection/read-model.md new file mode 100644 index 0000000..d994cdf --- /dev/null +++ b/Documentation/client-snippets/projections/declarative/simple-projection/read-model.md @@ -0,0 +1,7 @@ +```typescript +class DecSimpleUser { + name = ''; + email = ''; + createdAt = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/eventual-consistency/cqs.md b/Documentation/client-snippets/projections/eventual-consistency/cqs.md new file mode 100644 index 0000000..e9dea58 --- /dev/null +++ b/Documentation/client-snippets/projections/eventual-consistency/cqs.md @@ -0,0 +1,31 @@ +```typescript +import { eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class EcCqsBookCreated { + constructor(readonly title: string) {} +} + +class EcCqsBook { + id: string = ''; + title: string = ''; +} + +// Commands — fire and forget, never return projected state +class EcCqsBookCommandHandler { + constructor(private readonly store: IEventStore) {} + + create(bookId: string, title: string): Promise { + return this.store.eventLog.append(bookId, new EcCqsBookCreated(title)).then(() => undefined); + } +} + +// Queries — always read from projections +class EcCqsBookQueryHandler { + constructor(private readonly store: IEventStore) {} + + getBook(bookId: string): Promise { + return this.store.readModels.getInstanceById(EcCqsBook, bookId); + } +} +``` diff --git a/Documentation/client-snippets/projections/eventual-consistency/design-for-async.md b/Documentation/client-snippets/projections/eventual-consistency/design-for-async.md new file mode 100644 index 0000000..4dd5cf3 --- /dev/null +++ b/Documentation/client-snippets/projections/eventual-consistency/design-for-async.md @@ -0,0 +1,34 @@ +```typescript +import { eventType, Guid, IEventStore } from '@cratis/chronicle'; + +@eventType() +class EcBookCreated { + constructor(readonly title: string, readonly author: string) {} +} + +class EcBookInventory { + id: string = ''; + title: string = ''; + author: string = ''; +} + +class EcBookService { + constructor(private readonly store: IEventStore) {} + + // Good — fire and forget: don't wait for the projection before returning + async createBook(title: string, author: string): Promise { + const bookId = Guid.create().toString(); + await this.store.eventLog.append(bookId, new EcBookCreated(title, author)); + return bookId; + } + + // Problematic — expecting immediate consistency + async createBookAndReturn(title: string, author: string): Promise { + const bookId = Guid.create().toString(); + await this.store.eventLog.append(bookId, new EcBookCreated(title, author)); + + // The projection may not have run yet — this can return a stale or default instance + return this.store.readModels.getInstanceById(EcBookInventory, bookId); + } +} +``` diff --git a/Documentation/client-snippets/projections/eventual-consistency/watch.md b/Documentation/client-snippets/projections/eventual-consistency/watch.md new file mode 100644 index 0000000..74e9c46 --- /dev/null +++ b/Documentation/client-snippets/projections/eventual-consistency/watch.md @@ -0,0 +1,40 @@ +```typescript +import { eventType, Guid, IEventStore } from '@cratis/chronicle'; + +@eventType() +class EcWatchBookCreated { + constructor(readonly title: string, readonly author: string) {} +} + +class EcWatchBookInventory { + id: string = ''; + title: string = ''; + author: string = ''; +} + +class EcWatchBookService { + constructor(private readonly store: IEventStore) {} + + watchBookChanges() { + return this.store.readModels.watch(EcWatchBookInventory); + } + + async createBookAndWatch(title: string, author: string): Promise { + const bookId = Guid.create().toString(); + + // Start watching before appending so the update is observed once the projection catches up + const watchBook = async () => { + for await (const changeset of this.store.readModels.watch(EcWatchBookInventory)) { + if (changeset.key === bookId) { + console.log(`Book projection updated: ${changeset.readModel.title}`); + break; + } + } + }; + const watching = watchBook(); + + await this.store.eventLog.append(bookId, new EcWatchBookCreated(title, author)); + await watching; + } +} +``` diff --git a/Documentation/client-snippets/projections/filtering/appending-with-metadata.md b/Documentation/client-snippets/projections/filtering/appending-with-metadata.md new file mode 100644 index 0000000..d476c36 --- /dev/null +++ b/Documentation/client-snippets/projections/filtering/appending-with-metadata.md @@ -0,0 +1,7 @@ +```text +TypeScript does not support this workflow yet. +`AppendOptions` only carries `correlationId`, `eventSourceId`, and `concurrencyScope` — +there is no way to attach tags or a custom event stream type when appending from +TypeScript. Track the client SDK issue before relying on metadata-filtered +observers from TypeScript. +``` diff --git a/Documentation/client-snippets/projections/filtering/basic.md b/Documentation/client-snippets/projections/filtering/basic.md new file mode 100644 index 0000000..34eaa26 --- /dev/null +++ b/Documentation/client-snippets/projections/filtering/basic.md @@ -0,0 +1,23 @@ +```typescript +import { eventType, fromEvent, readModel } from '@cratis/chronicle'; + +@eventType() +class FilteringOrderPlaced { + customerId = ''; + totalAmount = 0; +} + +@eventType() +class FilteringOrderShipped { + shippedAt: Date | null = null; +} + +@readModel() +@fromEvent(FilteringOrderPlaced) +@fromEvent(FilteringOrderShipped) +class FilteringOrderSummary { + customerId = ''; + totalAmount = 0; + shippedAt: Date | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/basic-mapping/add-from.md b/Documentation/client-snippets/projections/model-bound/basic-mapping/add-from.md new file mode 100644 index 0000000..1c43e42 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/basic-mapping/add-from.md @@ -0,0 +1,22 @@ +```typescript title="Add from an event" +import { addFrom, eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class AccountOpenedForDeposits { + constructor(readonly initialBalance: number) {} +} + +@eventType() +class DepositMadeForBalance { + constructor(readonly amount: number) {} +} + +@readModel() +@fromEvent(AccountOpenedForDeposits) +@fromEvent(DepositMadeForBalance) +class DepositAccount { + @setFrom(AccountOpenedForDeposits, 'initialBalance') + @addFrom(DepositMadeForBalance, 'amount') + balance = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/basic-mapping/complete.md b/Documentation/client-snippets/projections/model-bound/basic-mapping/complete.md new file mode 100644 index 0000000..faa3816 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/basic-mapping/complete.md @@ -0,0 +1,39 @@ +```typescript title="Complete balance projection" +import { addFrom, eventType, fromEvent, readModel, setFrom, subtractFrom } from '@cratis/chronicle'; + +@eventType() +class BankAccountOpened { + constructor(readonly accountName: string, readonly initialBalance: number) {} +} + +@eventType() +class BankAccountRenamed { + constructor(readonly newName: string) {} +} + +@eventType() +class FundsDeposited { + constructor(readonly amount: number) {} +} + +@eventType() +class FundsWithdrawn { + constructor(readonly amount: number) {} +} + +@readModel() +@fromEvent(BankAccountOpened) +@fromEvent(BankAccountRenamed) +@fromEvent(FundsDeposited) +@fromEvent(FundsWithdrawn) +class BankAccount { + @setFrom(BankAccountOpened, 'accountName') + @setFrom(BankAccountRenamed, 'newName') + name = ''; + + @setFrom(BankAccountOpened, 'initialBalance') + @addFrom(FundsDeposited, 'amount') + @subtractFrom(FundsWithdrawn, 'amount') + balance = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/basic-mapping/context-vs-from-every.md b/Documentation/client-snippets/projections/model-bound/basic-mapping/context-vs-from-every.md new file mode 100644 index 0000000..d3141f7 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/basic-mapping/context-vs-from-every.md @@ -0,0 +1,27 @@ +```typescript title="Specific context vs every event" +import { eventType, fromEvent, fromEvery, readModel, setFromContext } from '@cratis/chronicle'; + +@eventType() +class OrderPlacedForLifecycle { + constructor(readonly customerName: string) {} +} + +@eventType() +class OrderShippedForLifecycle { + constructor(readonly trackingNumber: string) {} +} + +@readModel() +@fromEvent(OrderPlacedForLifecycle) +@fromEvent(OrderShippedForLifecycle) +class OrderLifecycle { + @setFromContext(OrderPlacedForLifecycle, 'occurred') + placedAt = new Date(); + + @setFromContext(OrderShippedForLifecycle, 'occurred') + shippedAt?: Date; + + @fromEvery(undefined, 'occurred') + lastModified = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-context.md b/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-context.md new file mode 100644 index 0000000..288d599 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-context.md @@ -0,0 +1,18 @@ +```typescript title="Map event context" +import { eventType, fromEvent, readModel, setFrom, setFromContext } from '@cratis/chronicle'; + +@eventType() +class OrderPlacedForAudit { + constructor(readonly customerName: string) {} +} + +@readModel() +@fromEvent(OrderPlacedForAudit) +class AuditedOrder { + @setFrom(OrderPlacedForAudit, 'customerName') + customerName = ''; + + @setFromContext(OrderPlacedForAudit, 'occurred') + orderedAt = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-convention.md b/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-convention.md new file mode 100644 index 0000000..1818892 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-convention.md @@ -0,0 +1,18 @@ +```typescript title="Convention-based set mapping" +import { eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class UserRegisteredForProfile { + constructor(readonly name: string, readonly email: string) {} +} + +@readModel() +@fromEvent(UserRegisteredForProfile) +class UserProfile { + @setFrom(UserRegisteredForProfile) + name = ''; + + @setFrom(UserRegisteredForProfile) + email = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-multiple-events.md b/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-multiple-events.md new file mode 100644 index 0000000..7a2b35d --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from-multiple-events.md @@ -0,0 +1,22 @@ +```typescript title="Multiple set mappings" +import { eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class AccountOpenedForRename { + constructor(readonly accountName: string) {} +} + +@eventType() +class AccountRenamedForRename { + constructor(readonly newName: string) {} +} + +@readModel() +@fromEvent(AccountOpenedForRename) +@fromEvent(AccountRenamedForRename) +class RenameableAccount { + @setFrom(AccountOpenedForRename, 'accountName') + @setFrom(AccountRenamedForRename, 'newName') + name = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from.md b/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from.md new file mode 100644 index 0000000..60d8002 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/basic-mapping/set-from.md @@ -0,0 +1,18 @@ +```typescript title="Model-bound set mapping" +import { eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class UserRegisteredForContact { + constructor(readonly name: string, readonly email: string) {} +} + +@readModel() +@fromEvent(UserRegisteredForContact) +class UserContact { + @setFrom(UserRegisteredForContact, 'email') + email = ''; + + @setFrom(UserRegisteredForContact, 'name') + name = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/basic-mapping/subtract-from.md b/Documentation/client-snippets/projections/model-bound/basic-mapping/subtract-from.md new file mode 100644 index 0000000..bff0f61 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/basic-mapping/subtract-from.md @@ -0,0 +1,29 @@ +```typescript title="Subtract from an event" +import { addFrom, eventType, fromEvent, readModel, setFrom, subtractFrom } from '@cratis/chronicle'; + +@eventType() +class BalanceAccountOpened { + constructor(readonly initialBalance: number) {} +} + +@eventType() +class BalanceDepositMade { + constructor(readonly amount: number) {} +} + +@eventType() +class BalanceWithdrawalMade { + constructor(readonly amount: number) {} +} + +@readModel() +@fromEvent(BalanceAccountOpened) +@fromEvent(BalanceDepositMade) +@fromEvent(BalanceWithdrawalMade) +class BalanceAccount { + @setFrom(BalanceAccountOpened, 'initialBalance') + @addFrom(BalanceDepositMade, 'amount') + @subtractFrom(BalanceWithdrawalMade, 'amount') + balance = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/children/automap.md b/Documentation/client-snippets/projections/model-bound/children/automap.md new file mode 100644 index 0000000..8be94d5 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/children/automap.md @@ -0,0 +1,26 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class MbChildrenAutoMapLineItemAdded { + itemId: Guid = Guid.empty; + productName = ''; + quantity = 0; + price = 0; +} + +@readModel() +class MbChildrenAutoMapOrder { + id: Guid = Guid.empty; + + @childrenFrom(MbChildrenAutoMapLineItemAdded, 'itemId') + items: MbChildrenAutoMapLineItem[] = []; +} + +class MbChildrenAutoMapLineItem { + id: Guid = Guid.empty; + productName = ''; // Automatically mapped from MbChildrenAutoMapLineItemAdded.productName + quantity = 0; // Automatically mapped from MbChildrenAutoMapLineItemAdded.quantity + price = 0; // Automatically mapped from MbChildrenAutoMapLineItemAdded.price +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/children/basic.md b/Documentation/client-snippets/projections/model-bound/children/basic.md new file mode 100644 index 0000000..ed50ac8 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/children/basic.md @@ -0,0 +1,27 @@ +```typescript +import { childrenFrom, eventType, Guid, readModel } from '@cratis/chronicle'; + +@eventType() +class MbChildrenLineItemAdded { + itemId: Guid = Guid.empty; + productName = ''; + quantity = 0; + price = 0; +} + +@readModel() +class MbChildrenOrder { + id: Guid = Guid.empty; + + @childrenFrom(MbChildrenLineItemAdded, 'itemId') + items: MbChildrenLineItem[] = []; +} + +// The `id` property is automatically discovered as the child's key +class MbChildrenLineItem { + id: Guid = Guid.empty; + productName = ''; + quantity = 0; + price = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/children/child-from-event.md b/Documentation/client-snippets/projections/model-bound/children/child-from-event.md new file mode 100644 index 0000000..96f57fc --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/children/child-from-event.md @@ -0,0 +1,32 @@ +```typescript +import { childrenFrom, eventType, fromEvent, Guid, readModel } from '@cratis/chronicle'; + +@eventType() +class MbChildrenChildFromEventConfigurationAdded { + dashboardId: Guid = Guid.empty; + configurationId: Guid = Guid.empty; + name = ''; +} + +@eventType() +class MbChildrenChildFromEventConfigurationRenamed { + dashboardId: Guid = Guid.empty; + id: Guid = Guid.empty; + name = ''; +} + +@readModel() +class MbChildrenChildFromEventDashboard { + id: Guid = Guid.empty; + name = ''; + + @childrenFrom(MbChildrenChildFromEventConfigurationAdded, 'configurationId', undefined, 'dashboardId') + configurations: MbChildrenChildFromEventConfiguration[] = []; +} + +@fromEvent(MbChildrenChildFromEventConfigurationRenamed, { parentKey: 'dashboardId' }) +class MbChildrenChildFromEventConfiguration { + id: Guid = Guid.empty; + name = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/children/full-example.md b/Documentation/client-snippets/projections/model-bound/children/full-example.md new file mode 100644 index 0000000..b580ca5 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/children/full-example.md @@ -0,0 +1,55 @@ +```typescript +import { childrenFrom, eventType, Guid, readModel, removedWith, setFrom } from '@cratis/chronicle'; + +// Events +@eventType() +class MbChildrenFullOrderCreated { + customerName = ''; +} + +@eventType() +class MbChildrenFullLineItemAdded { + itemId: Guid = Guid.empty; + productName = ''; + initialQuantity = 0; + unitPrice = 0; +} + +@eventType() +class MbChildrenFullQuantityAdjusted { + itemId: Guid = Guid.empty; + newQuantity = 0; +} + +@eventType() +class MbChildrenFullLineItemRemoved { + itemId: Guid = Guid.empty; +} + +// Read Models +@readModel() +class MbChildrenFullOrder { + id: Guid = Guid.empty; + + @setFrom(MbChildrenFullOrderCreated, 'customerName') + customer = ''; + + @childrenFrom(MbChildrenFullLineItemAdded, 'itemId') + @removedWith(MbChildrenFullLineItemRemoved, 'itemId') + lines: MbChildrenFullOrderLine[] = []; +} + +class MbChildrenFullOrderLine { + id: Guid = Guid.empty; + + @setFrom(MbChildrenFullLineItemAdded, 'productName') + product = ''; + + @setFrom(MbChildrenFullLineItemAdded, 'initialQuantity') + @setFrom(MbChildrenFullQuantityAdjusted, 'newQuantity') + quantity = 0; + + @setFrom(MbChildrenFullLineItemAdded, 'unitPrice') + unitPrice = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/children/nested-hierarchy.md b/Documentation/client-snippets/projections/model-bound/children/nested-hierarchy.md new file mode 100644 index 0000000..2991106 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/children/nested-hierarchy.md @@ -0,0 +1,65 @@ +```typescript +import { childrenFrom, eventType, Guid, join, readModel, setFrom } from '@cratis/chronicle'; + +// Events +@eventType() +class MbChildrenNestedOrganizationCreated { + name = ''; +} + +@eventType() +class MbChildrenNestedDepartmentAdded { + id: Guid = Guid.empty; + name = ''; +} + +@eventType() +class MbChildrenNestedDepartmentRenamed { + id: Guid = Guid.empty; + newName = ''; +} + +@eventType() +class MbChildrenNestedTeamAdded { + id: Guid = Guid.empty; + departmentId: Guid = Guid.empty; + name = ''; +} + +@eventType() +class MbChildrenNestedTeamRenamed { + id: Guid = Guid.empty; + newName = ''; +} + +// Read Models - all decorators work at every nesting level +@readModel() +class MbChildrenNestedOrganization { + id: Guid = Guid.empty; + + @setFrom(MbChildrenNestedOrganizationCreated, 'name') + name = ''; + + @childrenFrom(MbChildrenNestedDepartmentAdded, 'id', 'id') + departments: MbChildrenNestedDepartment[] = []; +} + +class MbChildrenNestedDepartment { + id: Guid = Guid.empty; + + @setFrom(MbChildrenNestedDepartmentAdded, 'name') + @join(MbChildrenNestedDepartmentRenamed, undefined, 'newName') // Joins work on children + name = ''; + + @childrenFrom(MbChildrenNestedTeamAdded, 'id', 'id', 'departmentId') // Nested children + teams: MbChildrenNestedTeam[] = []; +} + +class MbChildrenNestedTeam { + id: Guid = Guid.empty; + + @setFrom(MbChildrenNestedTeamAdded, 'name') + @join(MbChildrenNestedTeamRenamed, undefined, 'newName') // Joins work on nested children too + name = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/children/removal-class-level.md b/Documentation/client-snippets/projections/model-bound/children/removal-class-level.md new file mode 100644 index 0000000..5d7b8f8 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/children/removal-class-level.md @@ -0,0 +1,29 @@ +```typescript +import { childrenFrom, eventType, Guid, readModel, removedWith } from '@cratis/chronicle'; + +@eventType() +class MbChildrenRemovalClassLineItemAdded { + itemId: Guid = Guid.empty; + description = ''; +} + +@eventType() +class MbChildrenRemovalClassLineItemRemoved { + orderId: Guid = Guid.empty; + itemId: Guid = Guid.empty; +} + +@readModel() +class MbChildrenRemovalClassOrder { + id: Guid = Guid.empty; + + @childrenFrom(MbChildrenRemovalClassLineItemAdded, 'itemId') + lines: MbChildrenRemovalClassOrderLine[] = []; +} + +@removedWith(MbChildrenRemovalClassLineItemRemoved, 'itemId', 'orderId') +class MbChildrenRemovalClassOrderLine { + id: Guid = Guid.empty; + description = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/children/removal-property-level.md b/Documentation/client-snippets/projections/model-bound/children/removal-property-level.md new file mode 100644 index 0000000..8e967e8 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/children/removal-property-level.md @@ -0,0 +1,28 @@ +```typescript +import { childrenFrom, eventType, Guid, readModel, removedWith } from '@cratis/chronicle'; + +@eventType() +class MbChildrenRemovalPropertyLineItemAdded { + itemId: Guid = Guid.empty; + description = ''; +} + +@eventType() +class MbChildrenRemovalPropertyLineItemRemoved { + itemId: Guid = Guid.empty; +} + +@readModel() +class MbChildrenRemovalPropertyOrder { + id: Guid = Guid.empty; + + @childrenFrom(MbChildrenRemovalPropertyLineItemAdded, 'itemId') + @removedWith(MbChildrenRemovalPropertyLineItemRemoved, 'itemId') + lines: MbChildrenRemovalPropertyOrderLine[] = []; +} + +class MbChildrenRemovalPropertyOrderLine { + id: Guid = Guid.empty; + description = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/children/removed-with-join.md b/Documentation/client-snippets/projections/model-bound/children/removed-with-join.md new file mode 100644 index 0000000..b951e1a --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/children/removed-with-join.md @@ -0,0 +1,28 @@ +```typescript +import { childrenFrom, eventType, Guid, readModel, removedWithJoin } from '@cratis/chronicle'; + +@eventType() +class MbChildrenRemovedFeatureActivated { + featureId: Guid = Guid.empty; + name = ''; +} + +@eventType() +class MbChildrenRemovedFeatureDeactivated { + featureId: Guid = Guid.empty; +} + +@readModel() +class MbChildrenRemovedSubscription { + id: Guid = Guid.empty; + + @childrenFrom(MbChildrenRemovedFeatureActivated, 'featureId', 'featureId') + @removedWithJoin(MbChildrenRemovedFeatureDeactivated, 'featureId') + features: MbChildrenRemovedFeature[] = []; +} + +class MbChildrenRemovedFeature { + featureId: Guid = Guid.empty; + name = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/children/with-counters.md b/Documentation/client-snippets/projections/model-bound/children/with-counters.md new file mode 100644 index 0000000..8dce40d --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/children/with-counters.md @@ -0,0 +1,45 @@ +```typescript +import { childrenFrom, decrement, eventType, Guid, increment, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbChildrenCountersItemAddedToCart { + itemId: Guid = Guid.empty; + productName = ''; + price = 0; + initialQuantity = 0; +} + +@eventType() +class MbChildrenCountersQuantityIncreased { + itemId: Guid = Guid.empty; +} + +@eventType() +class MbChildrenCountersQuantityDecreased { + itemId: Guid = Guid.empty; +} + +@readModel() +class MbChildrenCountersShoppingCart { + id: Guid = Guid.empty; + + @childrenFrom(MbChildrenCountersItemAddedToCart, 'itemId') + items: MbChildrenCountersCartItem[] = []; +} + +// Child type with its own projection decorators +class MbChildrenCountersCartItem { + id: Guid = Guid.empty; + + @setFrom(MbChildrenCountersItemAddedToCart, 'productName') + productName = ''; + + @setFrom(MbChildrenCountersItemAddedToCart, 'price') + price = 0; + + @setFrom(MbChildrenCountersItemAddedToCart, 'initialQuantity') + @increment(MbChildrenCountersQuantityIncreased) + @decrement(MbChildrenCountersQuantityDecreased) + quantity = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/constant-key/basic.md b/Documentation/client-snippets/projections/model-bound/constant-key/basic.md new file mode 100644 index 0000000..3236eb0 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/constant-key/basic.md @@ -0,0 +1,19 @@ +```typescript +import { eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbConstantKeyOrderPlaced { + customerName = ''; + placedAt = new Date(); +} + +@readModel() +@fromEvent(MbConstantKeyOrderPlaced, { constantKey: 'global' }) +class MbConstantKeyGlobalOrderSummary { + @setFrom(MbConstantKeyOrderPlaced, 'customerName') + lastCustomer = ''; + + @setFrom(MbConstantKeyOrderPlaced, 'placedAt') + lastOrderDate = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/constant-key/counters.md b/Documentation/client-snippets/projections/model-bound/constant-key/counters.md new file mode 100644 index 0000000..bdafee2 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/constant-key/counters.md @@ -0,0 +1,32 @@ +```typescript +import { count, decrement, eventType, increment, readModel } from '@cratis/chronicle'; + +@eventType() +class MbConstantKeyOrderPlacedForMetrics { +} + +@eventType() +class MbConstantKeyUserLoggedIn { +} + +@eventType() +class MbConstantKeyUserLoggedOut { +} + +@eventType() +class MbConstantKeyErrorOccurred { +} + +@readModel() +class MbConstantKeySystemMetrics { + @count(MbConstantKeyOrderPlacedForMetrics, 'metrics') + totalOrders = 0; + + @increment(MbConstantKeyUserLoggedIn, 'metrics') + @decrement(MbConstantKeyUserLoggedOut, 'metrics') + activeSessions = 0; + + @count(MbConstantKeyErrorOccurred, 'metrics') + totalErrors = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/constant-key/full-example.md b/Documentation/client-snippets/projections/model-bound/constant-key/full-example.md new file mode 100644 index 0000000..15885dc --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/constant-key/full-example.md @@ -0,0 +1,38 @@ +```typescript +import { count, decrement, eventType, increment, readModel } from '@cratis/chronicle'; + +// Events +@eventType() +class MbConstantKeyProductPurchased { + productId = ''; + amount = 0; +} + +@eventType() +class MbConstantKeyProductReturned { + productId = ''; + amount = 0; +} + +@eventType() +class MbConstantKeyPageViewed { + pageUrl = ''; +} + +// Global read model +@readModel() +class MbConstantKeyStoreMetrics { + @count(MbConstantKeyProductPurchased, 'store') + totalPurchases = 0; + + @count(MbConstantKeyProductReturned, 'store') + totalReturns = 0; + + @increment(MbConstantKeyProductPurchased, 'store') + @decrement(MbConstantKeyProductReturned, 'store') + netTransactions = 0; + + @count(MbConstantKeyPageViewed, 'store') + totalPageViews = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/constant-key/mixed-keys.md b/Documentation/client-snippets/projections/model-bound/constant-key/mixed-keys.md new file mode 100644 index 0000000..df54923 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/constant-key/mixed-keys.md @@ -0,0 +1,22 @@ +```typescript +import { count, eventType, fromEvent, Guid, readModel } from '@cratis/chronicle'; + +@eventType() +class MbConstantKeyUserRegistered { +} + +@eventType() +class MbConstantKeyOrderPlacedGlobal { +} + +@readModel() +@fromEvent(MbConstantKeyUserRegistered) +class MbConstantKeyUserDashboard { + id: Guid = Guid.empty; + name = ''; + + // A per-instance property alongside a constant-keyed one on the same read model + @count(MbConstantKeyOrderPlacedGlobal, 'global-stats') + platformTotalOrders = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/convention-based/automap-equivalent.md b/Documentation/client-snippets/projections/model-bound/convention-based/automap-equivalent.md new file mode 100644 index 0000000..3ce2a26 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/convention-based/automap-equivalent.md @@ -0,0 +1,22 @@ +```typescript title="Model-bound and declarative AutoMap" +import { eventType, fromEvent, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class ConventionEquivalentUserRegistered { + constructor(readonly name: string, readonly email: string) {} +} + +@readModel() +@fromEvent(ConventionEquivalentUserRegistered) +class ConventionEquivalentUser { + name = ''; + email = ''; +} + +@projection('', ConventionEquivalentUser) +class ConventionEquivalentProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder.from(ConventionEquivalentUserRegistered); + } +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/convention-based/basic.md b/Documentation/client-snippets/projections/model-bound/convention-based/basic.md new file mode 100644 index 0000000..d33e7c7 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/convention-based/basic.md @@ -0,0 +1,20 @@ +```typescript title="Convention-based mapping" +import { eventType, fromEvent, readModel } from '@cratis/chronicle'; + +@eventType() +class ConventionUserRegistered { + constructor( + readonly name: string, + readonly email: string, + readonly registeredAt: Date + ) {} +} + +@readModel() +@fromEvent(ConventionUserRegistered) +class ConventionUser { + name = ''; + email = ''; + registeredAt = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/convention-based/custom-key.md b/Documentation/client-snippets/projections/model-bound/convention-based/custom-key.md new file mode 100644 index 0000000..f4cdf88 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/convention-based/custom-key.md @@ -0,0 +1,19 @@ +```typescript title="Custom key" +import { eventType, fromEvent, readModel } from '@cratis/chronicle'; + +@eventType() +class ConventionUserRegisteredWithKey { + constructor( + readonly userId: string, + readonly name: string, + readonly email: string + ) {} +} + +@readModel() +@fromEvent(ConventionUserRegisteredWithKey, { key: 'userId' }) +class ConventionUserById { + name = ''; + email = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/convention-based/explicit-equivalent.md b/Documentation/client-snippets/projections/model-bound/convention-based/explicit-equivalent.md new file mode 100644 index 0000000..80946ef --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/convention-based/explicit-equivalent.md @@ -0,0 +1,25 @@ +```typescript title="Equivalent explicit mappings" +import { eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class ExplicitConventionUserRegistered { + constructor( + readonly name: string, + readonly email: string, + readonly registeredAt: Date + ) {} +} + +@readModel() +@fromEvent(ExplicitConventionUserRegistered) +class ExplicitConventionUser { + @setFrom(ExplicitConventionUserRegistered, 'name') + name = ''; + + @setFrom(ExplicitConventionUserRegistered, 'email') + email = ''; + + @setFrom(ExplicitConventionUserRegistered, 'registeredAt') + registeredAt = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/convention-based/matching-structures.md b/Documentation/client-snippets/projections/model-bound/convention-based/matching-structures.md new file mode 100644 index 0000000..2549197 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/convention-based/matching-structures.md @@ -0,0 +1,51 @@ +```typescript title="Matching nested structures and collections" +import { eventType, fromEvent, readModel } from '@cratis/chronicle'; + +class ConventionAddress { + street = ''; + city = ''; + postalCode = ''; +} + +class ConventionLineItem { + productName = ''; + unitPrice = 0; + quantity = 0; +} + +@eventType() +class ConventionCustomerRegistered { + constructor( + readonly firstName: string, + readonly lastName: string, + readonly billingAddress: ConventionAddress, + readonly shippingAddress: ConventionAddress + ) {} +} + +@eventType() +class ConventionOrderCreated { + constructor( + readonly customerEmail: string, + readonly items: ConventionLineItem[], + readonly tags: string[] + ) {} +} + +@readModel() +@fromEvent(ConventionCustomerRegistered) +class ConventionCustomer { + firstName = ''; + lastName = ''; + billingAddress = new ConventionAddress(); + shippingAddress = new ConventionAddress(); +} + +@readModel() +@fromEvent(ConventionOrderCreated) +class ConventionOrder { + customerEmail = ''; + items: ConventionLineItem[] = []; + tags: string[] = []; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/convention-based/multiple-events.md b/Documentation/client-snippets/projections/model-bound/convention-based/multiple-events.md new file mode 100644 index 0000000..e009c8d --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/convention-based/multiple-events.md @@ -0,0 +1,26 @@ +```typescript title="Multiple convention events" +import { eventType, fromEvent, readModel } from '@cratis/chronicle'; + +@eventType() +class ConventionUserProfileCreated { + constructor(readonly name: string, readonly email: string) {} +} + +@eventType() +class ConventionUserProfileUpdated { + constructor( + readonly name: string, + readonly email: string, + readonly phone: string + ) {} +} + +@readModel() +@fromEvent(ConventionUserProfileCreated) +@fromEvent(ConventionUserProfileUpdated) +class ConventionUserProfile { + name = ''; + email = ''; + phone = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/convention-based/partial-events.md b/Documentation/client-snippets/projections/model-bound/convention-based/partial-events.md new file mode 100644 index 0000000..338ce9b --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/convention-based/partial-events.md @@ -0,0 +1,27 @@ +```typescript title="Partial event shapes" +import { eventType, fromEvent, readModel } from '@cratis/chronicle'; + +@eventType() +class ConventionPartialUserRegistered { + constructor(readonly email: string) {} +} + +@eventType() +class ConventionPartialUserCompleted { + constructor( + readonly firstName: string, + readonly lastName: string, + readonly phone: string + ) {} +} + +@readModel() +@fromEvent(ConventionPartialUserRegistered) +@fromEvent(ConventionPartialUserCompleted) +class ConventionPartialUser { + email = ''; + firstName = ''; + lastName = ''; + phone = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/counters/basic-increment.md b/Documentation/client-snippets/projections/model-bound/counters/basic-increment.md new file mode 100644 index 0000000..b0e6bd0 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/counters/basic-increment.md @@ -0,0 +1,15 @@ +```typescript +import { eventType, Guid, increment, readModel } from '@cratis/chronicle'; + +@eventType() +class MbCountersUserLoggedIn { +} + +@readModel() +class MbCountersUserStatistics { + id: Guid = Guid.empty; + + @increment(MbCountersUserLoggedIn) + loginCount = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/counters/combined.md b/Documentation/client-snippets/projections/model-bound/counters/combined.md new file mode 100644 index 0000000..7d997dc --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/counters/combined.md @@ -0,0 +1,36 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class MbCountersItemCreated { + name = ''; + initialQuantity = 0; +} + +@eventType() +class MbCountersItemRestocked { +} + +@eventType() +class MbCountersItemSold { +} + +@readModel() +class MbCountersInventoryItem { + id: Guid = Guid.empty; + + @setFrom(MbCountersItemCreated, 'name') + name = ''; + + @setFrom(MbCountersItemCreated, 'initialQuantity') + @increment(MbCountersItemRestocked) + @decrement(MbCountersItemSold) + quantity = 0; + + @count(MbCountersItemRestocked) + restockCount = 0; + + @count(MbCountersItemSold) + salesCount = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/counters/count.md b/Documentation/client-snippets/projections/model-bound/counters/count.md new file mode 100644 index 0000000..1f56ffc --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/counters/count.md @@ -0,0 +1,22 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class MbCountersOrderPlaced { +} + +@eventType() +class MbCountersOrderCancelled { +} + +@readModel() +class MbCountersEventMetrics { + id: Guid = Guid.empty; + + @count(MbCountersOrderPlaced) + totalOrders = 0; + + @count(MbCountersOrderCancelled) + cancelledOrders = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/counters/full-example.md b/Documentation/client-snippets/projections/model-bound/counters/full-example.md new file mode 100644 index 0000000..9d2fbb4 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/counters/full-example.md @@ -0,0 +1,54 @@ +```typescript +import { addFrom, count, decrement, eventType, Guid, increment, readModel, subtractFrom } from '@cratis/chronicle'; + +// Events +@eventType() +class MbCountersUserLoggedInFull { + timestamp = new Date(); +} + +@eventType() +class MbCountersUserLoggedOutFull { + timestamp = new Date(); +} + +@eventType() +class MbCountersPurchaseMade { + amount = 0; +} + +@eventType() +class MbCountersRefundIssued { + amount = 0; +} + +// Read Model +@readModel() +class MbCountersUserActivity { + id: Guid = Guid.empty; + + // Track login/logout counts + @count(MbCountersUserLoggedInFull) + totalLogins = 0; + + @count(MbCountersUserLoggedOutFull) + totalLogouts = 0; + + // Track active sessions + @increment(MbCountersUserLoggedInFull) + @decrement(MbCountersUserLoggedOutFull) + activeSessions = 0; + + // Track transaction counts + @count(MbCountersPurchaseMade) + purchaseCount = 0; + + @count(MbCountersRefundIssued) + refundCount = 0; + + // Track transaction values + @addFrom(MbCountersPurchaseMade, 'amount') + @subtractFrom(MbCountersRefundIssued, 'amount') + netSpent = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/counters/increment-decrement.md b/Documentation/client-snippets/projections/model-bound/counters/increment-decrement.md new file mode 100644 index 0000000..00cf242 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/counters/increment-decrement.md @@ -0,0 +1,20 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class MbCountersUserConnected { +} + +@eventType() +class MbCountersUserDisconnected { +} + +@readModel() +class MbCountersServerStatistics { + id: Guid = Guid.empty; + + @increment(MbCountersUserConnected) + @decrement(MbCountersUserDisconnected) + activeConnections = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/event-sequence-source/fluent.md b/Documentation/client-snippets/projections/model-bound/event-sequence-source/fluent.md new file mode 100644 index 0000000..f60bd78 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/event-sequence-source/fluent.md @@ -0,0 +1,21 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class MbEventSeqFluentOrderPlaced { + amount = 0; +} + +// The event sequence is configured on the decorator rather than the builder +@projection('', undefined, 'custom-sequence') +class MbEventSeqFluentOrderProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder.from(MbEventSeqFluentOrderPlaced, _ => _ + .set(m => m.totalAmount).to(e => e.amount)); + } +} + +class MbEventSeqFluentOrderSummary { + totalAmount = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/from-every/combined.md b/Documentation/client-snippets/projections/model-bound/from-every/combined.md new file mode 100644 index 0000000..b516ba2 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/from-every/combined.md @@ -0,0 +1,35 @@ +```typescript title="Combine specific mappings with every-event metadata" +import { eventType, fromEvent, fromEvery, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class UserRegisteredForEvery { + constructor(readonly name: string, readonly email: string) {} +} + +@eventType() +class UserNameChangedForEvery { + constructor(readonly newName: string) {} +} + +@eventType() +class UserEmailChangedForEvery { + constructor(readonly newEmail: string) {} +} + +@readModel() +@fromEvent(UserRegisteredForEvery) +@fromEvent(UserNameChangedForEvery) +@fromEvent(UserEmailChangedForEvery) +class UserProfileFromEvery { + @setFrom(UserRegisteredForEvery, 'name') + @setFrom(UserNameChangedForEvery, 'newName') + name = ''; + + @setFrom(UserRegisteredForEvery, 'email') + @setFrom(UserEmailChangedForEvery, 'newEmail') + email = ''; + + @fromEvery(undefined, 'occurred') + lastUpdated = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/from-every/context-fields.md b/Documentation/client-snippets/projections/model-bound/from-every/context-fields.md new file mode 100644 index 0000000..2331660 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/from-every/context-fields.md @@ -0,0 +1,21 @@ +```typescript title="Track audit metadata from every event" +import { eventType, fromEvent, fromEvery, readModel } from '@cratis/chronicle'; + +@eventType() +class AuditableInventoryChangedForEvery { + constructor(readonly reason: string) {} +} + +@readModel() +@fromEvent(AuditableInventoryChangedForEvery) +class AuditableInventoryStatusFromEvery { + @fromEvery(undefined, 'occurred') + lastModified = new Date(); + + @fromEvery(undefined, 'sequenceNumber') + lastEventSequence = 0n; + + @fromEvery(undefined, 'correlationId') + lastCorrelationId = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/from-every/context-property.md b/Documentation/client-snippets/projections/model-bound/from-every/context-property.md new file mode 100644 index 0000000..57a4f90 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/from-every/context-property.md @@ -0,0 +1,23 @@ +```typescript title="Update an audit timestamp from every event" +import { eventType, fromEvent, fromEvery, readModel } from '@cratis/chronicle'; + +@eventType() +class InventoryProductRegisteredForEvery { + constructor(readonly productName: string) {} +} + +@eventType() +class InventoryItemsAdjustedForEvery { + constructor(readonly quantity: number) {} +} + +@readModel() +@fromEvent(InventoryProductRegisteredForEvery) +@fromEvent(InventoryItemsAdjustedForEvery) +class InventoryStatusFromEvery { + productName = ''; + + @fromEvery(undefined, 'occurred') + lastUpdated = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/from-every/convention.md b/Documentation/client-snippets/projections/model-bound/from-every/convention.md new file mode 100644 index 0000000..1de485b --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/from-every/convention.md @@ -0,0 +1,24 @@ +```typescript title="Use the read model property name by convention" +import { eventType, fromEvent, fromEvery, readModel } from '@cratis/chronicle'; + +@eventType() +class ProductRenamedForEveryConvention { + constructor(readonly name: string, readonly version: number) {} +} + +@eventType() +class ProductPriceChangedForEveryConvention { + constructor(readonly price: number, readonly version: number) {} +} + +@readModel() +@fromEvent(ProductRenamedForEveryConvention) +@fromEvent(ProductPriceChangedForEveryConvention) +class ProductVersionFromEveryConvention { + name = ''; + price = 0; + + @fromEvery() + version = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/from-every/declarative-equivalent.md b/Documentation/client-snippets/projections/model-bound/from-every/declarative-equivalent.md new file mode 100644 index 0000000..c9bd3a9 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/from-every/declarative-equivalent.md @@ -0,0 +1,31 @@ +```typescript title="Declarative projection with every-event metadata" +import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle'; + +@eventType() +class InventoryRegisteredDeclarativeForEvery { + constructor(readonly productName: string) {} +} + +@eventType() +class InventoryAdjustedDeclarativeForEvery { + constructor(readonly quantity: number) {} +} + +@readModel() +class InventoryStatusDeclarativeFromEvery { + productName = ''; + lastUpdated = new Date(); +} + +@projection('', InventoryStatusDeclarativeFromEvery) +class InventoryStatusDeclarativeProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(InventoryRegisteredDeclarativeForEvery) + .from(InventoryAdjustedDeclarativeForEvery) + .fromEvery(_ => _ + .set(m => m.lastUpdated) + .toEventContextProperty('occurred')); + } +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/from-every/event-property.md b/Documentation/client-snippets/projections/model-bound/from-every/event-property.md new file mode 100644 index 0000000..6282c0d --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/from-every/event-property.md @@ -0,0 +1,27 @@ +```typescript title="Read a shared event property from every event" +import { eventType, fromEvent, fromEvery, readModel } from '@cratis/chronicle'; + +enum OrderStateFromEvery { + New = 'New', + Confirmed = 'Confirmed', + Shipped = 'Shipped' +} + +@eventType() +class OrderConfirmedForEvery { + constructor(readonly status: OrderStateFromEvery) {} +} + +@eventType() +class OrderShippedForEvery { + constructor(readonly status: OrderStateFromEvery) {} +} + +@readModel() +@fromEvent(OrderConfirmedForEvery) +@fromEvent(OrderShippedForEvery) +class OrderStatusFromEvery { + @fromEvery('status') + currentStatus = OrderStateFromEvery.New; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/index/automap-fluent.md b/Documentation/client-snippets/projections/model-bound/index/automap-fluent.md new file mode 100644 index 0000000..213cc0d --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/index/automap-fluent.md @@ -0,0 +1,21 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class MbIndexAutoMapAccountOpened { + name = ''; + balance = 0; +} + +@projection() +class MbIndexAutoMapAccountProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder.autoMap().from(MbIndexAutoMapAccountOpened); + } +} + +class MbIndexAutoMapAccountInfo { + name = ''; + balance = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/index/automap-model-bound.md b/Documentation/client-snippets/projections/model-bound/index/automap-model-bound.md new file mode 100644 index 0000000..ca2a9a6 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/index/automap-model-bound.md @@ -0,0 +1,11 @@ +```typescript +import { fromEvent, Guid, readModel } from '@cratis/chronicle'; + +@readModel() +@fromEvent(MbIndexAutoMapAccountOpened) +class MbIndexAutoMapMbAccountInfo { + id: Guid = Guid.empty; + name = ''; // Automatically mapped from MbIndexAutoMapAccountOpened.name + balance = 0; // Automatically mapped from MbIndexAutoMapAccountOpened.balance +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/index/basic.md b/Documentation/client-snippets/projections/model-bound/index/basic.md new file mode 100644 index 0000000..7959427 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/index/basic.md @@ -0,0 +1,19 @@ +```typescript +import { eventType, fromEvent, Guid, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbIndexAccountOpened { + name = ''; + initialBalance = 0; +} + +@readModel() +@fromEvent(MbIndexAccountOpened) +class MbIndexAccountInfo { + id: Guid = Guid.empty; + name = ''; + + @setFrom(MbIndexAccountOpened, 'initialBalance') + balance = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/index/explicit-mapping-fluent.md b/Documentation/client-snippets/projections/model-bound/index/explicit-mapping-fluent.md new file mode 100644 index 0000000..d482a99 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/index/explicit-mapping-fluent.md @@ -0,0 +1,23 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class MbIndexExplicitAccountOpened { + name = ''; + initialBalance = 0; +} + +@projection() +class MbIndexExplicitAccountProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder.from(MbIndexExplicitAccountOpened, _ => _ + .set(m => m.name).to(e => e.name) + .set(m => m.balance).to(e => e.initialBalance)); + } +} + +class MbIndexExplicitAccountInfo { + name = ''; + balance = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/index/explicit-mapping-model-bound.md b/Documentation/client-snippets/projections/model-bound/index/explicit-mapping-model-bound.md new file mode 100644 index 0000000..eb39f90 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/index/explicit-mapping-model-bound.md @@ -0,0 +1,13 @@ +```typescript +import { Guid, setFrom } from '@cratis/chronicle'; + +class MbIndexExplicitMbAccountInfo { + id: Guid = Guid.empty; + + @setFrom(MbIndexExplicitAccountOpened, 'name') + name = ''; + + @setFrom(MbIndexExplicitAccountOpened, 'initialBalance') + balance = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/joins/basic.md b/Documentation/client-snippets/projections/model-bound/joins/basic.md new file mode 100644 index 0000000..9153cfd --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/joins/basic.md @@ -0,0 +1,28 @@ +```typescript +import { eventType, Guid, join, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbJoinsOrderPlaced { + customerId: Guid = Guid.empty; + amount = 0; +} + +@eventType() +class MbJoinsCustomerCreated { + name = ''; +} + +@readModel() +class MbJoinsOrderSummary { + id: Guid = Guid.empty; + + @setFrom(MbJoinsOrderPlaced, 'amount') + amount = 0; + + @setFrom(MbJoinsOrderPlaced, 'customerId') + customerId: Guid = Guid.empty; + + @join(MbJoinsCustomerCreated, 'customerId', 'name') + customerName = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/joins/children.md b/Documentation/client-snippets/projections/model-bound/joins/children.md new file mode 100644 index 0000000..61930d3 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/joins/children.md @@ -0,0 +1,38 @@ +```typescript +import { childrenFrom, eventType, Guid, join, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbJoinsLineItemAdded { + productId: Guid = Guid.empty; + quantity = 0; +} + +@eventType() +class MbJoinsProductUpdated { + productName = ''; + currentPrice = 0; +} + +@readModel() +class MbJoinsOrder { + id: Guid = Guid.empty; + + @childrenFrom(MbJoinsLineItemAdded, 'productId') + lines: MbJoinsOrderLine[] = []; +} + +// The line's key is the product id, so the join to ProductUpdated (raised on that +// same product's event source) resolves implicitly through the child's own key. +class MbJoinsOrderLine { + id: Guid = Guid.empty; + + @setFrom(MbJoinsLineItemAdded, 'quantity') + quantity = 0; + + @join(MbJoinsProductUpdated, undefined, 'productName') + productName = ''; + + @join(MbJoinsProductUpdated, undefined, 'currentPrice') + price = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/joins/full-example.md b/Documentation/client-snippets/projections/model-bound/joins/full-example.md new file mode 100644 index 0000000..2ba7c2c --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/joins/full-example.md @@ -0,0 +1,79 @@ +```typescript +import { childrenFrom, eventType, Guid, join, readModel, setFrom } from '@cratis/chronicle'; + +// Events +@eventType() +class MbJoinsFullOrderPlaced { + customerId: Guid = Guid.empty; + placedAt = new Date(); +} + +@eventType() +class MbJoinsFullCustomerRegistered { + name = ''; + email = ''; +} + +@eventType() +class MbJoinsFullCustomerProfileUpdated { + phoneNumber = ''; +} + +@eventType() +class MbJoinsFullLineItemAdded { + productId: Guid = Guid.empty; + quantity = 0; +} + +@eventType() +class MbJoinsFullProductCreated { + name = ''; + price = 0; +} + +@eventType() +class MbJoinsFullProductPriceChanged { + newPrice = 0; +} + +// Read Models +@readModel() +class MbJoinsFullOrderDetails { + id: Guid = Guid.empty; + + @setFrom(MbJoinsFullOrderPlaced, 'placedAt') + placedAt = new Date(); + + @setFrom(MbJoinsFullOrderPlaced, 'customerId') + customerId: Guid = Guid.empty; + + // Join customer information + @join(MbJoinsFullCustomerRegistered, 'customerId', 'name') + customerName = ''; + + @join(MbJoinsFullCustomerRegistered, 'customerId', 'email') + customerEmail = ''; + + @join(MbJoinsFullCustomerProfileUpdated, 'customerId', 'phoneNumber') + customerPhone = ''; + + @childrenFrom(MbJoinsFullLineItemAdded, 'productId') + items: MbJoinsFullLineItemDetails[] = []; +} + +// Keyed by product id, so the joins below resolve implicitly through the child's own key. +class MbJoinsFullLineItemDetails { + id: Guid = Guid.empty; + + @setFrom(MbJoinsFullLineItemAdded, 'quantity') + quantity = 0; + + // Join product information + @join(MbJoinsFullProductCreated, undefined, 'name') + productName = ''; + + @join(MbJoinsFullProductCreated, undefined, 'price') + @join(MbJoinsFullProductPriceChanged, undefined, 'newPrice') + price = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/joins/multiple-join-sources.md b/Documentation/client-snippets/projections/model-bound/joins/multiple-join-sources.md new file mode 100644 index 0000000..481ac48 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/joins/multiple-join-sources.md @@ -0,0 +1,41 @@ +```typescript +import { childrenFrom, eventType, Guid, join, readModel } from '@cratis/chronicle'; + +@eventType() +class MbJoinsSourcesLineItemAdded { + productId: Guid = Guid.empty; +} + +@eventType() +class MbJoinsSourcesProductCatalogUpdated { + name = ''; + description = ''; +} + +@eventType() +class MbJoinsSourcesPricingUpdated { + currentPrice = 0; +} + +@readModel() +class MbJoinsSourcesOrder { + id: Guid = Guid.empty; + + @childrenFrom(MbJoinsSourcesLineItemAdded, 'productId') + lines: MbJoinsSourcesOrderLine[] = []; +} + +// Keyed by product id, so both joins below resolve implicitly through the child's own key. +class MbJoinsSourcesOrderLine { + id: Guid = Guid.empty; + + @join(MbJoinsSourcesProductCatalogUpdated, undefined, 'name') + productName = ''; + + @join(MbJoinsSourcesProductCatalogUpdated, undefined, 'description') + description = ''; + + @join(MbJoinsSourcesPricingUpdated, undefined, 'currentPrice') + unitPrice = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/joins/multiple-joins.md b/Documentation/client-snippets/projections/model-bound/joins/multiple-joins.md new file mode 100644 index 0000000..68c8331 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/joins/multiple-joins.md @@ -0,0 +1,42 @@ +```typescript +import { eventType, Guid, join, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbJoinsMultipleOrderPlaced { + customerId: Guid = Guid.empty; +} + +@eventType() +class MbJoinsMultipleCustomerCreated { + name = ''; +} + +@eventType() +class MbJoinsCustomerUpdated { + email = ''; +} + +@eventType() +class MbJoinsShippingAddressSet { + address = ''; +} + +@readModel() +class MbJoinsEnrichedOrder { + id: Guid = Guid.empty; + + @setFrom(MbJoinsMultipleOrderPlaced, 'customerId') + customerId: Guid = Guid.empty; + + @join(MbJoinsMultipleCustomerCreated, 'customerId', 'name') + customerName = ''; + + @join(MbJoinsCustomerUpdated, 'customerId', 'email') + customerEmail = ''; + + // ShippingAddressSet is raised on the order's own event source, so it joins on the + // read model's own key rather than a separate correlating property. + @join(MbJoinsShippingAddressSet, 'id', 'address') + shippingAddress = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/not-rewindable/basic.md b/Documentation/client-snippets/projections/model-bound/not-rewindable/basic.md new file mode 100644 index 0000000..ff8bb00 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/not-rewindable/basic.md @@ -0,0 +1,21 @@ +```typescript +import { eventType, Guid, notRewindable, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbNotRewindableAuditEvent { + message = ''; + occurredAt = new Date(); +} + +@readModel() +@notRewindable +class MbNotRewindableAuditLog { + id: Guid = Guid.empty; + + @setFrom(MbNotRewindableAuditEvent, 'message') + message = ''; + + @setFrom(MbNotRewindableAuditEvent, 'occurredAt') + timestamp = new Date(); +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/passive/basic.md b/Documentation/client-snippets/projections/model-bound/passive/basic.md new file mode 100644 index 0000000..96a0aba --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/passive/basic.md @@ -0,0 +1,17 @@ +```typescript +import { eventType, Guid, passive, readModel, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbPassiveSnapshotCreated { + data = ''; +} + +@readModel() +@passive +class MbPassiveHistoricalSnapshot { + id: Guid = Guid.empty; + + @setFrom(MbPassiveSnapshotCreated, 'data') + data = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/removal/basic.md b/Documentation/client-snippets/projections/model-bound/removal/basic.md new file mode 100644 index 0000000..934ff62 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/removal/basic.md @@ -0,0 +1,25 @@ +```typescript +import { eventType, Guid, readModel, removedWith, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbRemovalAccountOpened { + name = ''; + balance = 0; +} + +@eventType() +class MbRemovalAccountClosed { +} + +@readModel() +@removedWith(MbRemovalAccountClosed) +class MbRemovalAccount { + id: Guid = Guid.empty; + + @setFrom(MbRemovalAccountOpened, 'name') + name = ''; + + @setFrom(MbRemovalAccountOpened, 'balance') + balance = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/removal/full-example.md b/Documentation/client-snippets/projections/model-bound/removal/full-example.md new file mode 100644 index 0000000..29be070 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/removal/full-example.md @@ -0,0 +1,55 @@ +```typescript +import { childrenFrom, eventType, Guid, readModel, removedWith, setFrom } from '@cratis/chronicle'; + +// Events +@eventType() +class MbRemovalFullShoppingCartCreated { + customerName = ''; +} + +@eventType() +class MbRemovalFullItemAddedToCart { + itemId: Guid = Guid.empty; + productName = ''; + price = 0; +} + +@eventType() +class MbRemovalFullItemRemovedFromCart { + cartId: Guid = Guid.empty; + itemId: Guid = Guid.empty; +} + +@eventType() +class MbRemovalFullCartCheckedOut { +} + +@eventType() +class MbRemovalFullCartAbandoned { +} + +// Read Models +@readModel() +@removedWith(MbRemovalFullCartCheckedOut) +@removedWith(MbRemovalFullCartAbandoned) +class MbRemovalFullShoppingCart { + id: Guid = Guid.empty; + + @setFrom(MbRemovalFullShoppingCartCreated, 'customerName') + customer = ''; + + @childrenFrom(MbRemovalFullItemAddedToCart, 'itemId') + items: MbRemovalFullCartItem[] = []; +} + +@removedWith(MbRemovalFullItemRemovedFromCart, 'itemId', 'cartId') +class MbRemovalFullCartItem { + id: Guid = Guid.empty; + + @setFrom(MbRemovalFullItemAddedToCart, 'productName') + product = ''; + + @setFrom(MbRemovalFullItemAddedToCart, 'price') + price = 0; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/removal/join-on-class.md b/Documentation/client-snippets/projections/model-bound/removal/join-on-class.md new file mode 100644 index 0000000..174f503 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/removal/join-on-class.md @@ -0,0 +1,29 @@ +```typescript +import { eventType, Guid, join, readModel, removedWithJoin, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbRemovalJoinClassEmployeeHired { + name = ''; +} + +@eventType() +class MbRemovalJoinClassCompanyRegistered { + name = ''; +} + +@eventType() +class MbRemovalJoinClassCompanyDissolved { +} + +@readModel() +@removedWithJoin(MbRemovalJoinClassCompanyDissolved) +class MbRemovalJoinClassEmployee { + id: Guid = Guid.empty; + + @setFrom(MbRemovalJoinClassEmployeeHired, 'name') + name = ''; + + @join(MbRemovalJoinClassCompanyRegistered, undefined, 'name') + companyName = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/removal/multiple-triggers.md b/Documentation/client-snippets/projections/model-bound/removal/multiple-triggers.md new file mode 100644 index 0000000..e4a19f2 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/removal/multiple-triggers.md @@ -0,0 +1,32 @@ +```typescript +import { eventType, Guid, readModel, removedWith, removedWithJoin, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbRemovalMultipleAccountOpened { + name = ''; +} + +@eventType() +class MbRemovalMultipleAccountClosed { +} + +@eventType() +class MbRemovalMultipleAccountMerged { + sourceAccountId: Guid = Guid.empty; +} + +@eventType() +class MbRemovalMultipleOrganizationClosed { +} + +@readModel() +@removedWith(MbRemovalMultipleAccountClosed) +@removedWith(MbRemovalMultipleAccountMerged, 'sourceAccountId') +@removedWithJoin(MbRemovalMultipleOrganizationClosed) +class MbRemovalMultipleAccount { + id: Guid = Guid.empty; + + @setFrom(MbRemovalMultipleAccountOpened, 'name') + name = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/removal/with-key.md b/Documentation/client-snippets/projections/model-bound/removal/with-key.md new file mode 100644 index 0000000..26dfdbe --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/removal/with-key.md @@ -0,0 +1,22 @@ +```typescript +import { eventType, Guid, readModel, removedWith, setFrom } from '@cratis/chronicle'; + +@eventType() +class MbRemovalWithKeyAccountOpened { + name = ''; +} + +@eventType() +class MbRemovalWithKeyAccountClosed { + accountId: Guid = Guid.empty; +} + +@readModel() +@removedWith(MbRemovalWithKeyAccountClosed, 'accountId') +class MbRemovalWithKeyAccount { + id: Guid = Guid.empty; + + @setFrom(MbRemovalWithKeyAccountOpened, 'name') + name = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/set-value/invoice.md b/Documentation/client-snippets/projections/model-bound/set-value/invoice.md new file mode 100644 index 0000000..49297e3 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/set-value/invoice.md @@ -0,0 +1,26 @@ +```typescript +import { eventType, fromEvent, Guid, readModel, setFrom, setValue } from '@cratis/chronicle'; + +@eventType() +class MbSetValueInvoiceIssued { + amount = 0; +} + +@eventType() +class MbSetValueInvoicePaid { +} + +@readModel() +@fromEvent(MbSetValueInvoiceIssued) +@fromEvent(MbSetValueInvoicePaid) +class MbSetValueInvoice { + id: Guid = Guid.empty; + + @setFrom(MbSetValueInvoiceIssued, 'amount') + amount = 0; + + @setValue(MbSetValueInvoiceIssued, 'issued') + @setValue(MbSetValueInvoicePaid, 'paid') + status = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/set-value/order.md b/Documentation/client-snippets/projections/model-bound/set-value/order.md new file mode 100644 index 0000000..22270b7 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/set-value/order.md @@ -0,0 +1,26 @@ +```typescript +import { eventType, fromEvent, Guid, readModel, setFrom, setValue } from '@cratis/chronicle'; + +@eventType() +class MbSetValueOrderPlaced { + customerName = ''; +} + +@eventType() +class MbSetValueOrderCanceled { +} + +@readModel() +@fromEvent(MbSetValueOrderPlaced) +@fromEvent(MbSetValueOrderCanceled) +class MbSetValueOrder { + id: Guid = Guid.empty; + + @setFrom(MbSetValueOrderPlaced, 'customerName') + customerName = ''; + + @setValue(MbSetValueOrderPlaced, 'active') + @setValue(MbSetValueOrderCanceled, 'canceled') + status = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/set-value/subscription.md b/Documentation/client-snippets/projections/model-bound/set-value/subscription.md new file mode 100644 index 0000000..b842da3 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/set-value/subscription.md @@ -0,0 +1,28 @@ +```typescript +import { eventType, fromEvent, Guid, readModel, setValue } from '@cratis/chronicle'; + +@eventType() +class MbSetValueSubscriptionStarted { +} + +@eventType() +class MbSetValueSubscriptionPaused { +} + +@eventType() +class MbSetValueSubscriptionCanceled { +} + +@readModel() +@fromEvent(MbSetValueSubscriptionStarted) +@fromEvent(MbSetValueSubscriptionPaused) +@fromEvent(MbSetValueSubscriptionCanceled) +class MbSetValueSubscription { + id: Guid = Guid.empty; + + @setValue(MbSetValueSubscriptionStarted, 'active') + @setValue(MbSetValueSubscriptionPaused, 'paused') + @setValue(MbSetValueSubscriptionCanceled, 'canceled') + state = ''; +} +``` diff --git a/Documentation/client-snippets/projections/model-bound/set-value/thing.md b/Documentation/client-snippets/projections/model-bound/set-value/thing.md new file mode 100644 index 0000000..b5e2181 --- /dev/null +++ b/Documentation/client-snippets/projections/model-bound/set-value/thing.md @@ -0,0 +1,25 @@ +```typescript +import { eventType, fromEvent, Guid, readModel, setValue } from '@cratis/chronicle'; + +@eventType() +class MbSetValueThingHappened { +} + +@readModel() +@fromEvent(MbSetValueThingHappened) +class MbSetValueThing { + id: Guid = Guid.empty; + + @setValue(MbSetValueThingHappened, 'pending') + statusLabel = ''; + + @setValue(MbSetValueThingHappened, 42) + priority = 0; + + @setValue(MbSetValueThingHappened, true) + isActive = false; + + @setValue(MbSetValueThingHappened, 3.14) + score = 0; +} +``` diff --git a/Documentation/client-snippets/projections/nested-objects-design/declarative.md b/Documentation/client-snippets/projections/nested-objects-design/declarative.md new file mode 100644 index 0000000..ed10826 --- /dev/null +++ b/Documentation/client-snippets/projections/nested-objects-design/declarative.md @@ -0,0 +1,38 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class NodDeclarativeSliceCreated { + constructor(readonly name: string) {} +} + +@eventType() +class NodDeclarativeCommandSet { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class NodDeclarativeCommandCleared { +} + +class NodDeclarativeCommandItem { + name = ''; + schema = ''; +} + +class NodDeclarativeSlice { + name = ''; + command: NodDeclarativeCommandItem | null = null; +} + +@projection() +class NodDeclarativeSliceProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(NodDeclarativeSliceCreated) + .nested(m => m.command, nested => nested + .from(NodDeclarativeCommandSet) + .clearWith(NodDeclarativeCommandCleared)); + } +} +``` diff --git a/Documentation/client-snippets/projections/nested-objects-design/model-bound.md b/Documentation/client-snippets/projections/nested-objects-design/model-bound.md new file mode 100644 index 0000000..ddb4de8 --- /dev/null +++ b/Documentation/client-snippets/projections/nested-objects-design/model-bound.md @@ -0,0 +1,34 @@ +```typescript +import { clearWith, eventType, fromEvent, Guid, nested, readModel } from '@cratis/chronicle'; + +@eventType() +class NodSliceCreated { + constructor(readonly name: string) {} +} + +@eventType() +class NodCommandSetForSlice { + constructor(readonly name: string, readonly schema: string) {} +} + +@eventType() +class NodCommandClearedForSlice { +} + +@fromEvent(NodCommandSetForSlice) +@clearWith(NodCommandClearedForSlice) +class NodCommandItem { + name = ''; + schema = ''; +} + +@readModel() +@fromEvent(NodSliceCreated) +class NodSlice { + id: Guid = Guid.empty; + name = ''; + + @nested + command: NodCommandItem | null = null; +} +``` diff --git a/Documentation/client-snippets/projections/nested-objects-design/recursive.md b/Documentation/client-snippets/projections/nested-objects-design/recursive.md new file mode 100644 index 0000000..4988c33 --- /dev/null +++ b/Documentation/client-snippets/projections/nested-objects-design/recursive.md @@ -0,0 +1,54 @@ +```typescript +import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle'; + +@eventType() +class NodRecursiveSliceCreated { + constructor(readonly name: string) {} +} + +@eventType() +class NodRecursiveCommandSet { + constructor(readonly name: string) {} +} + +@eventType() +class NodRecursiveCommandCleared { +} + +@eventType() +class NodRecursiveValidationConfigured { + constructor(readonly rules: string) {} +} + +@eventType() +class NodRecursiveValidationRemoved { +} + +class NodRecursiveValidationItem { + rules = ''; +} + +class NodRecursiveCommandItem { + name = ''; + validation: NodRecursiveValidationItem | null = null; +} + +class NodRecursiveSlice { + name = ''; + command: NodRecursiveCommandItem | null = null; +} + +@projection() +class NodRecursiveSliceProjection implements IProjectionFor { + define(builder: IProjectionBuilderFor): void { + builder + .from(NodRecursiveSliceCreated) + .nested(m => m.command, nested => nested + .from(NodRecursiveCommandSet) + .nested(m => m.validation, inner => inner + .from(NodRecursiveValidationConfigured) + .clearWith(NodRecursiveValidationRemoved)) + .clearWith(NodRecursiveCommandCleared)); + } +} +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/auto-map/event.md b/Documentation/client-snippets/projections/projection-declaration-language/auto-map/event.md new file mode 100644 index 0000000..c70f004 --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/auto-map/event.md @@ -0,0 +1,10 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class PdlAutoMapUserRegistered { + name = ''; + email = ''; + age = 0; +} +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/auto-map/read-model.md b/Documentation/client-snippets/projections/projection-declaration-language/auto-map/read-model.md new file mode 100644 index 0000000..6c340f7 --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/auto-map/read-model.md @@ -0,0 +1,8 @@ +```typescript +class PdlAutoMapUserReadModel { + name = ''; + email = ''; + age = 0; + isActive = false; +} +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/children/read-model.md b/Documentation/client-snippets/projections/projection-declaration-language/children/read-model.md new file mode 100644 index 0000000..45b3c53 --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/children/read-model.md @@ -0,0 +1,12 @@ +```typescript +class PdlGroupMember { + userId = ''; + name = ''; + role = ''; +} + +class PdlGroupReadModel { + name = ''; + members: PdlGroupMember[] = []; +} +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/counters/read-model.md b/Documentation/client-snippets/projections/projection-declaration-language/counters/read-model.md new file mode 100644 index 0000000..0fd60d2 --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/counters/read-model.md @@ -0,0 +1,7 @@ +```typescript +class PdlCounterUserReadModel { + name = ''; + loginCount = 0; // Initialize to 0 + profileViews = 0; +} +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/expressions/read-model.md b/Documentation/client-snippets/projections/projection-declaration-language/expressions/read-model.md new file mode 100644 index 0000000..36b5e0c --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/expressions/read-model.md @@ -0,0 +1,8 @@ +```typescript +class PdlExpressionsUserReadModel { + name = ''; // Requires string + loginCount = 0; // Requires number + isActive = false; // Requires boolean + createdAt = new Date(); // Requires timestamp +} +``` diff --git a/Documentation/client-snippets/projections/projection-declaration-language/nested/read-model.md b/Documentation/client-snippets/projections/projection-declaration-language/nested/read-model.md new file mode 100644 index 0000000..010cfb1 --- /dev/null +++ b/Documentation/client-snippets/projections/projection-declaration-language/nested/read-model.md @@ -0,0 +1,11 @@ +```typescript +class PdlCommandItem { + name = ''; + schema = ''; +} + +class PdlSliceReadModel { + name = ''; + command: PdlCommandItem | null = null; // null until set +} +``` diff --git a/Documentation/client-snippets/reactors/event-processing/event-context.md b/Documentation/client-snippets/reactors/event-processing/event-context.md new file mode 100644 index 0000000..7a243dc --- /dev/null +++ b/Documentation/client-snippets/reactors/event-processing/event-context.md @@ -0,0 +1,19 @@ +```typescript +import { EventContext, eventType, reactor } from '@cratis/chronicle'; + +@eventType() +class ReactorAccountClosed { + constructor(readonly accountId: string) {} +} + +@reactor() +class AuditReactor { + // Method name must be the exact camelCase of the event's class name - + // Chronicle discovers handlers by name, not by parameter type. + async reactorAccountClosed(event: ReactorAccountClosed, context: EventContext): Promise { + this.writeAudit(event.accountId, context.occurred, context.eventSourceId); + } + + private writeAudit(accountId: string, occurred: Date, eventSourceId: string): void {} +} +``` diff --git a/Documentation/client-snippets/reactors/event-sequence/reactor-attribute.md b/Documentation/client-snippets/reactors/event-sequence/reactor-attribute.md new file mode 100644 index 0000000..4812b8e --- /dev/null +++ b/Documentation/client-snippets/reactors/event-sequence/reactor-attribute.md @@ -0,0 +1,19 @@ +```typescript +import { eventType, reactor, EventContext } from '@cratis/chronicle'; + +@eventType() +class EventSequenceReactorAttributeShipmentDispatched { + constructor(readonly trackingNumber: string) {} +} + +@reactor('shipment-reactor', 'fulfillment-events') +class EventSequenceReactorAttributeShipmentReactor { + // Method name must be the exact camelCase of the event's class name - + // Chronicle discovers handlers by name, not by parameter type. + async eventSequenceReactorAttributeShipmentDispatched(event: EventSequenceReactorAttributeShipmentDispatched, context: EventContext): Promise { + await this.notifyCarrier(event.trackingNumber); + } + + private async notifyCarrier(trackingNumber: string): Promise {} +} +``` diff --git a/Documentation/client-snippets/reactors/getting-started/event.md b/Documentation/client-snippets/reactors/getting-started/event.md new file mode 100644 index 0000000..ed19421 --- /dev/null +++ b/Documentation/client-snippets/reactors/getting-started/event.md @@ -0,0 +1,11 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class ReactorOrderPlaced { + constructor( + readonly customerEmail: string, + readonly totalAmount: number + ) {} +} +``` diff --git a/Documentation/client-snippets/reactors/getting-started/reactor.md b/Documentation/client-snippets/reactors/getting-started/reactor.md new file mode 100644 index 0000000..44442ef --- /dev/null +++ b/Documentation/client-snippets/reactors/getting-started/reactor.md @@ -0,0 +1,21 @@ +```typescript +import { EventContext, reactor } from '@cratis/chronicle'; + +interface ReactorEmailGateway { + sendOrderPlaced(email: string, amount: number, occurred: Date): Promise; +} + +@reactor() +class OrderNotificationsReactor { + constructor(private readonly emailGateway: ReactorEmailGateway) {} + + // Method name must be the exact camelCase of the event's class name - + // Chronicle discovers handlers by name, not by parameter type. + async reactorOrderPlaced(event: ReactorOrderPlaced, context: EventContext): Promise { + await this.emailGateway.sendOrderPlaced( + event.customerEmail, + event.totalAmount, + context.occurred); + } +} +``` diff --git a/Documentation/client-snippets/reactors/getting-started/register.md b/Documentation/client-snippets/reactors/getting-started/register.md new file mode 100644 index 0000000..7451aa8 --- /dev/null +++ b/Documentation/client-snippets/reactors/getting-started/register.md @@ -0,0 +1,9 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ReactorRegistration { + async register(store: IEventStore): Promise { + await store.reactors.register(); + } +} +``` diff --git a/Documentation/client-snippets/reactors/getting-started/stable-id.md b/Documentation/client-snippets/reactors/getting-started/stable-id.md new file mode 100644 index 0000000..2775be0 --- /dev/null +++ b/Documentation/client-snippets/reactors/getting-started/stable-id.md @@ -0,0 +1,12 @@ +```typescript +import { reactor } from '@cratis/chronicle'; + +@reactor('order-notifications') +class NamedOrderNotificationsReactor { + // Method name must be the exact camelCase of the event's class name - + // Chronicle discovers handlers by name, not by parameter type. + async reactorOrderPlaced(_event: ReactorOrderPlaced): Promise { + // Perform the side effect. + } +} +``` diff --git a/Documentation/client-snippets/reactors/index/basic-example.md b/Documentation/client-snippets/reactors/index/basic-example.md new file mode 100644 index 0000000..df44674 --- /dev/null +++ b/Documentation/client-snippets/reactors/index/basic-example.md @@ -0,0 +1,19 @@ +```typescript +import { eventType, reactor, EventContext } from '@cratis/chronicle'; + +@eventType() +class ReactorsIndexEmailConfirmed { + constructor(readonly email: string) {} +} + +@reactor() +class ReactorsIndexEmailNotificationsReactor { + // Method name must be the exact camelCase of the event's class name - + // Chronicle discovers handlers by name, not by parameter type. + async reactorsIndexEmailConfirmed(event: ReactorsIndexEmailConfirmed, context: EventContext): Promise { + await this.sendConfirmation(event.email, context.occurred); + } + + private async sendConfirmation(email: string, occurred: Date): Promise {} +} +``` diff --git a/Documentation/client-snippets/read-models/getting-collection-instances/basic.md b/Documentation/client-snippets/read-models/getting-collection-instances/basic.md new file mode 100644 index 0000000..d41e002 --- /dev/null +++ b/Documentation/client-snippets/read-models/getting-collection-instances/basic.md @@ -0,0 +1,7 @@ +```typescript +const accounts = await store.readModels.getInstances(Account); + +for (const account of accounts) { + console.log(`${account.name}: ${account.balance}`); +} +``` diff --git a/Documentation/client-snippets/read-models/getting-collection-instances/event-count.md b/Documentation/client-snippets/read-models/getting-collection-instances/event-count.md new file mode 100644 index 0000000..82150cf --- /dev/null +++ b/Documentation/client-snippets/read-models/getting-collection-instances/event-count.md @@ -0,0 +1,5 @@ +```typescript +const orders = await store.readModels.getInstances(Order, 1000n); + +console.log(`Replayed ${orders.length} orders from the capped history.`); +``` diff --git a/Documentation/client-snippets/read-models/getting-collection-instances/filtering.md b/Documentation/client-snippets/read-models/getting-collection-instances/filtering.md new file mode 100644 index 0000000..32fee42 --- /dev/null +++ b/Documentation/client-snippets/read-models/getting-collection-instances/filtering.md @@ -0,0 +1,7 @@ +```typescript +const accounts = await store.readModels.getInstances(Account); + +const highValueAccounts = accounts + .filter((account) => account.balance > threshold) + .sort((left, right) => right.balance - left.balance); +``` diff --git a/Documentation/client-snippets/read-models/getting-single-instance/basic.md b/Documentation/client-snippets/read-models/getting-single-instance/basic.md new file mode 100644 index 0000000..174cc80 --- /dev/null +++ b/Documentation/client-snippets/read-models/getting-single-instance/basic.md @@ -0,0 +1,5 @@ +```typescript +const account = await store.readModels.getInstanceById(AccountInfo, accountId); + +console.log(`${account.name}: ${account.balance}`); +``` diff --git a/Documentation/client-snippets/read-models/getting-snapshots/analyze.md b/Documentation/client-snippets/read-models/getting-snapshots/analyze.md new file mode 100644 index 0000000..117b10e --- /dev/null +++ b/Documentation/client-snippets/read-models/getting-snapshots/analyze.md @@ -0,0 +1,10 @@ +```typescript +const snapshots = await store.readModels.getSnapshotsById(Order, orderId); + +for (const snapshot of snapshots) { + console.log(`Snapshot at ${snapshot.occurred?.toISOString() ?? 'unknown'}`); + console.log(` Correlation ID: ${snapshot.correlationId ?? 'none'}`); + console.log(` Event count: ${snapshot.events.length}`); + console.log(' State:', snapshot.readModel); +} +``` diff --git a/Documentation/client-snippets/read-models/getting-snapshots/basic.md b/Documentation/client-snippets/read-models/getting-snapshots/basic.md new file mode 100644 index 0000000..a7904c7 --- /dev/null +++ b/Documentation/client-snippets/read-models/getting-snapshots/basic.md @@ -0,0 +1,5 @@ +```typescript +const snapshots = await store.readModels.getSnapshotsById(Order, orderId); + +console.log(`Found ${snapshots.length} snapshots.`); +``` diff --git a/Documentation/client-snippets/read-models/materialized-pagination/accessing-api.md b/Documentation/client-snippets/read-models/materialized-pagination/accessing-api.md new file mode 100644 index 0000000..67d11dc --- /dev/null +++ b/Documentation/client-snippets/read-models/materialized-pagination/accessing-api.md @@ -0,0 +1,19 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class MaterializedPaginationOrder { + constructor( + readonly customerName: string, + readonly total: number + ) {} +} + +class MaterializedPaginationAccessingApi { + constructor(private readonly store: IEventStore) {} + + // Inject IEventStore, then reach through to the materialized API + async getOrders(): Promise { + return this.store.readModels.materialized.getInstances(MaterializedPaginationOrder); + } +} +``` diff --git a/Documentation/client-snippets/read-models/materialized-pagination/basic-usage.md b/Documentation/client-snippets/read-models/materialized-pagination/basic-usage.md new file mode 100644 index 0000000..463db0c --- /dev/null +++ b/Documentation/client-snippets/read-models/materialized-pagination/basic-usage.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class MaterializedPaginationBasicUsage { + constructor(private readonly store: IEventStore) {} + + async getOrders(): Promise { + return this.store.readModels.materialized.getInstances(MaterializedPaginationOrder); + } +} +``` diff --git a/Documentation/client-snippets/read-models/materialized-pagination/observing.md b/Documentation/client-snippets/read-models/materialized-pagination/observing.md new file mode 100644 index 0000000..4fe9475 --- /dev/null +++ b/Documentation/client-snippets/read-models/materialized-pagination/observing.md @@ -0,0 +1,21 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class MaterializedPaginationProduct { + constructor( + readonly name: string, + readonly price: number + ) {} +} + +class MaterializedPaginationObserving { + constructor(private readonly store: IEventStore) {} + + async run(): Promise { + // Called whenever the stored instances change + for await (const products of this.store.readModels.materialized.observeInstances(MaterializedPaginationProduct, 0, 50)) { + console.log(`Products updated: ${products.length} in view`); + } + } +} +``` diff --git a/Documentation/client-snippets/read-models/materialized-pagination/pagination.md b/Documentation/client-snippets/read-models/materialized-pagination/pagination.md new file mode 100644 index 0000000..9eae30d --- /dev/null +++ b/Documentation/client-snippets/read-models/materialized-pagination/pagination.md @@ -0,0 +1,21 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class MaterializedPaginationPagination { + constructor(private readonly store: IEventStore) {} + + async getPages(): Promise { + // First page of 20 + const page1 = await this.store.readModels.materialized.getInstances(MaterializedPaginationOrder, 0, 20); + console.log(`Page 1: ${page1.length} orders`); + + // Second page of 20 + const page2 = await this.store.readModels.materialized.getInstances(MaterializedPaginationOrder, 20, 20); + console.log(`Page 2: ${page2.length} orders`); + + // Third page of 20 + const page3 = await this.store.readModels.materialized.getInstances(MaterializedPaginationOrder, 40, 20); + console.log(`Page 3: ${page3.length} orders`); + } +} +``` diff --git a/Documentation/client-snippets/read-models/watching-read-models/basic.md b/Documentation/client-snippets/read-models/watching-read-models/basic.md new file mode 100644 index 0000000..91303c0 --- /dev/null +++ b/Documentation/client-snippets/read-models/watching-read-models/basic.md @@ -0,0 +1,9 @@ +```typescript +for await (const changeset of store.readModels.watch(Order)) { + if (changeset.removed) { + continue; + } + + console.log(`${changeset.key}: ${changeset.readModel.status}`); +} +``` diff --git a/Documentation/client-snippets/read-models/watching-read-models/filtering.md b/Documentation/client-snippets/read-models/watching-read-models/filtering.md new file mode 100644 index 0000000..fc9c39c --- /dev/null +++ b/Documentation/client-snippets/read-models/watching-read-models/filtering.md @@ -0,0 +1,9 @@ +```typescript +for await (const changeset of store.readModels.watch(Order)) { + if (changeset.readModel.totalAmount <= threshold) { + continue; + } + + console.log(`${changeset.key}: ${changeset.readModel.totalAmount}`); +} +``` diff --git a/Documentation/client-snippets/reducers/event-sequence/reducer-attribute.md b/Documentation/client-snippets/reducers/event-sequence/reducer-attribute.md new file mode 100644 index 0000000..068af0f --- /dev/null +++ b/Documentation/client-snippets/reducers/event-sequence/reducer-attribute.md @@ -0,0 +1,26 @@ +```typescript +import { eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class ReducersEventSequenceReducerAttributeShipmentDispatched { + trackingNumber = ''; + carrier = ''; +} + +class ReducersEventSequenceReducerAttributeShipmentSummary { + trackingNumber = ''; + carrier = ''; + dispatchedAt: Date = new Date(0); +} + +@reducer('shipment-summary', 'fulfillment-events', ReducersEventSequenceReducerAttributeShipmentSummary) +class ReducersEventSequenceReducerAttributeShipmentSummaryReducer { + // Method name must be the exact camelCase of the event's class name + reducersEventSequenceReducerAttributeShipmentDispatched( + event: ReducersEventSequenceReducerAttributeShipmentDispatched, + _current: ReducersEventSequenceReducerAttributeShipmentSummary | undefined + ): ReducersEventSequenceReducerAttributeShipmentSummary { + return { trackingNumber: event.trackingNumber, carrier: event.carrier, dispatchedAt: new Date() }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/getting-started/read-model.md b/Documentation/client-snippets/reducers/getting-started/read-model.md new file mode 100644 index 0000000..1701f2a --- /dev/null +++ b/Documentation/client-snippets/reducers/getting-started/read-model.md @@ -0,0 +1,8 @@ +```typescript +class ReducersGettingStartedOrderSummary { + orderId = ''; + totalAmount = 0; + itemCount = 0; + lastUpdated: Date = new Date(0); +} +``` diff --git a/Documentation/client-snippets/reducers/getting-started/reducer-attribute.md b/Documentation/client-snippets/reducers/getting-started/reducer-attribute.md new file mode 100644 index 0000000..7deb9d5 --- /dev/null +++ b/Documentation/client-snippets/reducers/getting-started/reducer-attribute.md @@ -0,0 +1,11 @@ +```typescript +import { reducer } from '@cratis/chronicle'; + +class ReducersGettingStartedAttributeOrderSummary { + orderId = ''; +} + +@reducer('order-summary', 'order-events', ReducersGettingStartedAttributeOrderSummary) +class ReducersGettingStartedAttributeOrderSummaryReducer { +} +``` diff --git a/Documentation/client-snippets/reducers/getting-started/reducer-implementation.md b/Documentation/client-snippets/reducers/getting-started/reducer-implementation.md new file mode 100644 index 0000000..771340d --- /dev/null +++ b/Documentation/client-snippets/reducers/getting-started/reducer-implementation.md @@ -0,0 +1,60 @@ +```typescript +import { eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class ReducersGettingStartedOrderCreated { + orderId = ''; +} + +@eventType() +class ReducersGettingStartedItemAddedToOrder { + price = 0; + quantity = 0; +} + +@eventType() +class ReducersGettingStartedItemRemovedFromOrder { + price = 0; + quantity = 0; +} + +// Method names must be the exact camelCase of the event's class name - +// Chronicle discovers handlers by name, not by parameter type. +@reducer('', undefined, ReducersGettingStartedOrderSummary) +class ReducersGettingStartedOrderSummaryReducer { + reducersGettingStartedOrderCreated( + event: ReducersGettingStartedOrderCreated, + _current: ReducersGettingStartedOrderSummary | undefined + ): ReducersGettingStartedOrderSummary { + return { orderId: event.orderId, totalAmount: 0, itemCount: 0, lastUpdated: new Date() }; + } + + reducersGettingStartedItemAddedToOrder( + event: ReducersGettingStartedItemAddedToOrder, + current: ReducersGettingStartedOrderSummary | undefined + ): ReducersGettingStartedOrderSummary | undefined { + if (!current) return undefined; // Skip if order not created yet + + return { + ...current, + totalAmount: current.totalAmount + (event.price * event.quantity), + itemCount: current.itemCount + event.quantity, + lastUpdated: new Date() + }; + } + + reducersGettingStartedItemRemovedFromOrder( + event: ReducersGettingStartedItemRemovedFromOrder, + current: ReducersGettingStartedOrderSummary | undefined + ): ReducersGettingStartedOrderSummary | undefined { + if (!current) return undefined; // Skip if order not created yet + + return { + ...current, + totalAmount: current.totalAmount - (event.price * event.quantity), + itemCount: current.itemCount - event.quantity, + lastUpdated: new Date() + }; + } +} +``` diff --git a/Documentation/client-snippets/reducers/getting-started/retrieving-state.md b/Documentation/client-snippets/reducers/getting-started/retrieving-state.md new file mode 100644 index 0000000..88cd963 --- /dev/null +++ b/Documentation/client-snippets/reducers/getting-started/retrieving-state.md @@ -0,0 +1,11 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ReducersGettingStartedOrderService { + constructor(private readonly store: IEventStore) {} + + async getOrderSummary(orderId: string): Promise { + return this.store.readModels.getInstanceById(ReducersGettingStartedOrderSummary, orderId); + } +} +``` diff --git a/Documentation/client-snippets/reducers/index/basic-example.md b/Documentation/client-snippets/reducers/index/basic-example.md new file mode 100644 index 0000000..1ea8eb8 --- /dev/null +++ b/Documentation/client-snippets/reducers/index/basic-example.md @@ -0,0 +1,39 @@ +```typescript +import { eventType, reducer } from '@cratis/chronicle'; + +@eventType() +class ReducersIndexDepositMade { + amount = 0; +} + +@eventType() +class ReducersIndexWithdrawalMade { + amount = 0; +} + +class ReducersIndexAccountBalance { + balance = 0; + lastUpdated: Date = new Date(0); +} + +// Handler method names must be the exact camelCase of the event's class name - +// Chronicle discovers handlers by name, not by parameter type. +@reducer('', undefined, ReducersIndexAccountBalance) +class ReducersIndexAccountBalanceReducer { + reducersIndexDepositMade( + event: ReducersIndexDepositMade, + current: ReducersIndexAccountBalance | undefined + ): ReducersIndexAccountBalance { + const currentBalance = current?.balance ?? 0; + return { balance: currentBalance + event.amount, lastUpdated: new Date() }; + } + + reducersIndexWithdrawalMade( + event: ReducersIndexWithdrawalMade, + current: ReducersIndexAccountBalance | undefined + ): ReducersIndexAccountBalance { + const currentBalance = current?.balance ?? 0; + return { balance: currentBalance - event.amount, lastUpdated: new Date() }; + } +} +``` diff --git a/Documentation/client-snippets/scenarios/react-to-an-event/basic-reactor.md b/Documentation/client-snippets/scenarios/react-to-an-event/basic-reactor.md new file mode 100644 index 0000000..96469db --- /dev/null +++ b/Documentation/client-snippets/scenarios/react-to-an-event/basic-reactor.md @@ -0,0 +1,25 @@ +```typescript +import { EventContext, eventType, reactor } from '@cratis/chronicle'; + +@eventType() +class ScenariosReactBookReturned { + constructor(readonly isbn: string) {} +} + +interface ScenariosReactNotificationService { + notifyNextInLine(bookId: string): Promise; + notifyNextInLine(bookId: string, bookTitle: string): Promise; +} + +@reactor() +class ScenariosReactWaitlistNotifier { + constructor(private readonly notifications: ScenariosReactNotificationService) {} + + // Method name must be the exact camelCase of the event's class name - + // Chronicle discovers handlers by name, not by parameter type. + async scenariosReactBookReturned(event: ScenariosReactBookReturned, context: EventContext): Promise { + // context.eventSourceId is the source the event happened to (the book) + await this.notifications.notifyNextInLine(context.eventSourceId); + } +} +``` diff --git a/Documentation/client-snippets/scenarios/react-to-an-event/explicit-append.md b/Documentation/client-snippets/scenarios/react-to-an-event/explicit-append.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/scenarios/react-to-an-event/explicit-append.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/scenarios/react-to-an-event/reading-state.md b/Documentation/client-snippets/scenarios/react-to-an-event/reading-state.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/scenarios/react-to-an-event/reading-state.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/scenarios/real-time-query/get-one.md b/Documentation/client-snippets/scenarios/real-time-query/get-one.md new file mode 100644 index 0000000..d0902fc --- /dev/null +++ b/Documentation/client-snippets/scenarios/real-time-query/get-one.md @@ -0,0 +1,18 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class ScenariosQueryBook { + constructor( + readonly title: string, + readonly onLoan: boolean + ) {} +} + +class ScenariosQueryBookService { + constructor(private readonly store: IEventStore) {} + + async getBook(bookId: string): Promise { + return this.store.readModels.getInstanceById(ScenariosQueryBook, bookId); + } +} +``` diff --git a/Documentation/client-snippets/subscriptions/explicit-subscriptions/basic.md b/Documentation/client-snippets/subscriptions/explicit-subscriptions/basic.md new file mode 100644 index 0000000..6443b6b --- /dev/null +++ b/Documentation/client-snippets/subscriptions/explicit-subscriptions/basic.md @@ -0,0 +1,18 @@ +```typescript +import { eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class SubscriptionsExplicitShipmentDispatched { + constructor(readonly orderId: string) {} +} + +class SubscriptionsExplicitBasic { + static async run(store: IEventStore): Promise { + await store.subscriptions.subscribe( + 'orders-from-fulfillment', + 'fulfillment-service', + builder => builder.withEventType(SubscriptionsExplicitShipmentDispatched) + ); + } +} +``` diff --git a/Documentation/client-snippets/subscriptions/explicit-subscriptions/filtering.md b/Documentation/client-snippets/subscriptions/explicit-subscriptions/filtering.md new file mode 100644 index 0000000..ed89df1 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/explicit-subscriptions/filtering.md @@ -0,0 +1,23 @@ +```typescript +import { eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class SubscriptionsExplicitStockReserved { + constructor( + readonly itemId: string, + readonly quantity: number + ) {} +} + +class SubscriptionsExplicitFiltering { + static async run(store: IEventStore): Promise { + await store.subscriptions.subscribe( + 'inventory-updates', + 'warehouse-service', + builder => builder + .withEventType(SubscriptionsExplicitStockAdjusted) + .withEventType(SubscriptionsExplicitStockReserved) + ); + } +} +``` diff --git a/Documentation/client-snippets/subscriptions/explicit-subscriptions/inbox-reactor.md b/Documentation/client-snippets/subscriptions/explicit-subscriptions/inbox-reactor.md new file mode 100644 index 0000000..c8446ee --- /dev/null +++ b/Documentation/client-snippets/subscriptions/explicit-subscriptions/inbox-reactor.md @@ -0,0 +1,22 @@ +```typescript +import { eventType, reactor } from '@cratis/chronicle'; + +@eventType() +class SubscriptionsExplicitOrderPlaced { + constructor( + readonly orderId: string, + readonly amount: number + ) {} +} + +@reactor() +class SubscriptionsExplicitIncomingOrdersReactor { + // Method name must be the exact camelCase of the event's class name - + // Chronicle discovers handlers by name, not by parameter type. + async subscriptionsExplicitOrderPlaced(event: SubscriptionsExplicitOrderPlaced): Promise { + await this.handleIncomingOrder(event.orderId, event.amount); + } + + private async handleIncomingOrder(orderId: string, amount: number): Promise {} +} +``` diff --git a/Documentation/client-snippets/subscriptions/explicit-subscriptions/naming-convention.md b/Documentation/client-snippets/subscriptions/explicit-subscriptions/naming-convention.md new file mode 100644 index 0000000..e2f99db --- /dev/null +++ b/Documentation/client-snippets/subscriptions/explicit-subscriptions/naming-convention.md @@ -0,0 +1,28 @@ +```typescript +import { eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class SubscriptionsExplicitStockAdjusted { + constructor( + readonly itemId: string, + readonly delta: number + ) {} +} + +class SubscriptionsExplicitNamingConvention { + static async run(store: IEventStore): Promise { + // subscription-id format: {target}-from-{source} + await store.subscriptions.subscribe( + 'orders-from-fulfillment', + 'fulfillment-service', + builder => builder.withEventType(SubscriptionsExplicitShipmentDispatched) + ); + + await store.subscriptions.subscribe( + 'inventory-from-warehouse', + 'warehouse-service', + builder => builder.withEventType(SubscriptionsExplicitStockAdjusted) + ); + } +} +``` diff --git a/Documentation/client-snippets/subscriptions/explicit-subscriptions/no-filter.md b/Documentation/client-snippets/subscriptions/explicit-subscriptions/no-filter.md new file mode 100644 index 0000000..9356a69 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/explicit-subscriptions/no-filter.md @@ -0,0 +1,10 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class SubscriptionsExplicitNoFilter { + static async run(store: IEventStore): Promise { + // All events from fulfillment-service outbox will be forwarded + await store.subscriptions.subscribe('all-fulfillment-events', 'fulfillment-service'); + } +} +``` diff --git a/Documentation/client-snippets/subscriptions/explicit-subscriptions/unsubscribe.md b/Documentation/client-snippets/subscriptions/explicit-subscriptions/unsubscribe.md new file mode 100644 index 0000000..c16678c --- /dev/null +++ b/Documentation/client-snippets/subscriptions/explicit-subscriptions/unsubscribe.md @@ -0,0 +1,9 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; + +class SubscriptionsExplicitUnsubscribe { + static async run(store: IEventStore): Promise { + await store.subscriptions.unsubscribe('orders-from-fulfillment'); + } +} +``` diff --git a/Documentation/client-snippets/subscriptions/outbox-inbox/inbox-reactor.md b/Documentation/client-snippets/subscriptions/outbox-inbox/inbox-reactor.md new file mode 100644 index 0000000..7cb6600 --- /dev/null +++ b/Documentation/client-snippets/subscriptions/outbox-inbox/inbox-reactor.md @@ -0,0 +1,20 @@ +```typescript +import { eventType, reactor } from '@cratis/chronicle'; + +@eventType() +class SubscriptionsOutboxInboxOrderPlaced { + constructor(readonly orderId: string) {} +} + +@reactor() +class SubscriptionsOutboxInboxIncomingOrdersReactor { + // Method name must be the exact camelCase of the event's class name - + // Chronicle discovers handlers by name, not by parameter type. + async subscriptionsOutboxInboxOrderPlaced(event: SubscriptionsOutboxInboxOrderPlaced): Promise { + // Handles OrderPlaced events from any subscribed source event store + await this.process(event.orderId); + } + + private async process(orderId: string): Promise {} +} +``` diff --git a/Documentation/client-snippets/tutorial/first-event/append.md b/Documentation/client-snippets/tutorial/first-event/append.md new file mode 100644 index 0000000..c5d94ad --- /dev/null +++ b/Documentation/client-snippets/tutorial/first-event/append.md @@ -0,0 +1,11 @@ +```typescript +import { Guid, IEventStore } from '@cratis/chronicle'; + +class TutorialFirstEventAppend { + async addBook(eventStore: IEventStore): Promise { + const bookId = Guid.create().toString(); + await eventStore.eventLog.append(bookId, new BookAdded('The Pragmatic Programmer', '978-0135957059')); + return bookId; + } +} +``` diff --git a/Documentation/client-snippets/tutorial/first-event/book-added.md b/Documentation/client-snippets/tutorial/first-event/book-added.md new file mode 100644 index 0000000..f183c18 --- /dev/null +++ b/Documentation/client-snippets/tutorial/first-event/book-added.md @@ -0,0 +1,11 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class BookAdded { + constructor( + readonly title: string, + readonly isbn: string + ) {} +} +``` diff --git a/Documentation/client-snippets/tutorial/first-event/book-id.md b/Documentation/client-snippets/tutorial/first-event/book-id.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/tutorial/first-event/book-id.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/tutorial/reacting/explicit-append.md b/Documentation/client-snippets/tutorial/reacting/explicit-append.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/tutorial/reacting/explicit-append.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/tutorial/reacting/notification-sent-event.md b/Documentation/client-snippets/tutorial/reacting/notification-sent-event.md new file mode 100644 index 0000000..916d666 --- /dev/null +++ b/Documentation/client-snippets/tutorial/reacting/notification-sent-event.md @@ -0,0 +1,7 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class WaitlistNotificationSent { +} +``` diff --git a/Documentation/client-snippets/tutorial/reacting/once-only.md b/Documentation/client-snippets/tutorial/reacting/once-only.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/tutorial/reacting/once-only.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/tutorial/reacting/reading-state.md b/Documentation/client-snippets/tutorial/reacting/reading-state.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/tutorial/reacting/reading-state.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/tutorial/reacting/side-effect.md b/Documentation/client-snippets/tutorial/reacting/side-effect.md new file mode 100644 index 0000000..19b3207 --- /dev/null +++ b/Documentation/client-snippets/tutorial/reacting/side-effect.md @@ -0,0 +1,3 @@ +```text +TypeScript does not support this workflow yet. +``` diff --git a/Documentation/client-snippets/tutorial/reacting/waitlist-notifier.md b/Documentation/client-snippets/tutorial/reacting/waitlist-notifier.md new file mode 100644 index 0000000..c036a82 --- /dev/null +++ b/Documentation/client-snippets/tutorial/reacting/waitlist-notifier.md @@ -0,0 +1,17 @@ +```typescript +import { EventContext, reactor } from '@cratis/chronicle'; + +async function notifyNextInLine(bookId: string): Promise { + console.log(`Notify next in line for book ${bookId}`); +} + +@reactor() +class WaitlistNotifier { + // Method name must be the exact camelCase of the event's class name - + // Chronicle discovers handlers by name, not by parameter type. + async bookReturned(event: BookReturned, context: EventContext): Promise { + // context.eventSourceId is the bookId this happened to + await notifyNextInLine(context.eventSourceId); + } +} +``` diff --git a/Documentation/client-snippets/tutorial/read-model/a-events.md b/Documentation/client-snippets/tutorial/read-model/a-events.md new file mode 100644 index 0000000..6a2342e --- /dev/null +++ b/Documentation/client-snippets/tutorial/read-model/a-events.md @@ -0,0 +1,12 @@ +```typescript +import { eventType } from '@cratis/chronicle'; + +@eventType() +class BookBorrowed { + constructor(readonly memberName: string) {} +} + +@eventType() +class BookReturned { +} +``` diff --git a/Documentation/client-snippets/tutorial/read-model/book.md b/Documentation/client-snippets/tutorial/read-model/book.md new file mode 100644 index 0000000..f05a798 --- /dev/null +++ b/Documentation/client-snippets/tutorial/read-model/book.md @@ -0,0 +1,23 @@ +```typescript +import { fromEvent, Guid, readModel, setFrom, setValue } from '@cratis/chronicle'; + +@readModel() +@fromEvent(BookAdded) +class Book { + id: Guid = Guid.empty; + + @setFrom(BookAdded, 'title') + title = ''; + + @setFrom(BookAdded, 'isbn') + isbn = ''; + + @setValue(BookAdded, false) + @setValue(BookBorrowed, true) + @setValue(BookReturned, false) + onLoan = false; + + @setFrom(BookBorrowed, 'memberName') + borrowedBy: string | null = null; +} +``` diff --git a/Documentation/client-snippets/tutorial/read-model/borrowed-book.md b/Documentation/client-snippets/tutorial/read-model/borrowed-book.md new file mode 100644 index 0000000..cdbd188 --- /dev/null +++ b/Documentation/client-snippets/tutorial/read-model/borrowed-book.md @@ -0,0 +1,13 @@ +```typescript +import { fromEvent, Guid, readModel, removedWith, setFrom } from '@cratis/chronicle'; + +@readModel() +@fromEvent(BookBorrowed) +@removedWith(BookReturned) +class BorrowedBook { + id: Guid = Guid.empty; + + @setFrom(BookBorrowed, 'memberName') + memberName = ''; +} +``` diff --git a/Documentation/client-snippets/understanding-constraints/dedicated-class.md b/Documentation/client-snippets/understanding-constraints/dedicated-class.md new file mode 100644 index 0000000..36b7c12 --- /dev/null +++ b/Documentation/client-snippets/understanding-constraints/dedicated-class.md @@ -0,0 +1,31 @@ +```typescript +import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle'; + +@eventType() +class UcDedicatedUserRegistered { + constructor(readonly email: string, readonly displayName: string) {} +} + +@eventType() +class UcDedicatedUserEmailChanged { + constructor(readonly newEmail: string) {} +} + +@eventType() +class UcDedicatedUserRemoved { +} + +@constraint() +class UcDedicatedUniqueEmail implements IConstraint { + define(builder: IConstraintBuilder): void { + builder.unique(unique => + unique + .withName('UniqueEmail') + .on(UcDedicatedUserRegistered, e => e.email) + .on(UcDedicatedUserEmailChanged, e => e.newEmail) + .ignoreCasing() + .removedWith(UcDedicatedUserRemoved) + .withMessage('That email address is already in use.')); + } +} +``` diff --git a/Documentation/client-snippets/webhooks/index/query.md b/Documentation/client-snippets/webhooks/index/query.md new file mode 100644 index 0000000..2f1eac7 --- /dev/null +++ b/Documentation/client-snippets/webhooks/index/query.md @@ -0,0 +1,12 @@ +```typescript +import { IEventStore } from '@cratis/chronicle'; +import { WebhookDefinition } from '@cratis/chronicle.contracts'; + +class WebhooksIndexQuery { + constructor(private readonly store: IEventStore) {} + + async getAllWebhooks(): Promise { + return this.store.webhooks.getWebhooks(); + } +} +``` diff --git a/Documentation/client-snippets/webhooks/index/register.md b/Documentation/client-snippets/webhooks/index/register.md new file mode 100644 index 0000000..f745025 --- /dev/null +++ b/Documentation/client-snippets/webhooks/index/register.md @@ -0,0 +1,28 @@ +```typescript +import { eventType, IEventStore } from '@cratis/chronicle'; + +@eventType() +class WebhooksIndexAccountOpened { + constructor(readonly ownerName: string) {} +} + +class WebhooksIndexRegister { + constructor( + private readonly store: IEventStore, + private readonly webhookToken: string + ) {} + + async registerWebhook(): Promise { + await this.store.webhooks.register( + 'account-events', + 'https://example.com/chronicle/webhooks', + builder => { + builder + .withEventType(WebhooksIndexAccountOpened) + .withHeader('x-source', 'my-app') + .withBearerToken(this.webhookToken); + } + ); + } +} +``` diff --git a/Documentation/compliance/index.md b/Documentation/compliance/index.md index c88e71e..5644fe2 100644 --- a/Documentation/compliance/index.md +++ b/Documentation/compliance/index.md @@ -1,149 +1,12 @@ -# Compliance - -Chronicle TypeScript provides built-in support for compliance requirements, particularly for handling Personal Identifiable Information (PII) according to GDPR definitions. The compliance system ensures that sensitive data is properly marked, encrypted, and managed throughout your application. - -## Overview - -The compliance support in Chronicle TypeScript includes: - -- **Type-level decorators** for marking ConceptAs types as containing sensitive information (recommended) -- **Property-level decorators** for marking individual properties -- **Automatic schema metadata** that informs the Chronicle Kernel about compliance requirements -- **Encryption** of compliance-annotated properties by the Chronicle Kernel -- **Release functionality** for decrypting properties when needed (with proper authorization) - -## Why Compliance Matters - -When building event-sourced systems, it's crucial to: - -1. **Identify sensitive data** - Know which properties contain PII -2. **Protect sensitive data** - Ensure PII is encrypted at rest -3. **Control access** - Only decrypt PII when authorized and necessary -4. **Comply with regulations** - Meet GDPR and other privacy requirements - -Chronicle's compliance support handles the technical aspects of these requirements, allowing you to focus on your business logic while maintaining regulatory compliance. - -## Key Concepts - -### Compliance Metadata - -Compliance metadata is information attached to your data structures that describes the compliance requirements. This metadata is: - -- Defined using decorators on your TypeScript classes -- Automatically included in JSON schemas sent to the Chronicle Kernel -- Used by the Kernel to apply appropriate encryption and access controls - -### Personal Identifiable Information (PII) - -PII is any data that can be used to identify an individual person. This includes: - -- Names -- Email addresses -- Phone numbers -- Social security numbers -- IP addresses -- And many other types of identifying information - -Chronicle provides the `@pii` decorator to mark properties or types containing such information. - -## Getting Started - -To use compliance features in your Chronicle application: - -1. **Import the compliance module and ConceptAs**: +--- +sharedTopicBridge: true +--- -```typescript -import { ConceptAs, field } from '@cratis/fundamentals'; -import { pii } from '@cratis/chronicle/compliance'; -``` - -2. **Define PII ConceptAs types** (recommended approach): - -```typescript -import { ConceptAs, field } from '@cratis/fundamentals'; -import { readModel, eventType, pii } from '@cratis/chronicle'; - -// Define PII types once -@pii('Employee social security number') -class EmployeeSSNConcept extends ConceptAs {} -export type EmployeeSSN = EmployeeSSNConcept | string; - -@pii('Personal email address') -class EmployeeEmailConcept extends ConceptAs {} -export type EmployeeEmail = EmployeeEmailConcept | string; - -// Use in events - PII metadata flows automatically -@eventType() -class EmployeeRegistered { - @field(String) - employeeId!: string; - - @field(EmployeeSSNConcept) - ssn!: EmployeeSSN; // Automatically PII - - @field(EmployeeEmailConcept) - email!: EmployeeEmail; // Automatically PII -} - -// Use in read models - same PII metadata flows automatically -@readModel() -class Employee { - @field(String) - id: string = ''; - - @field(EmployeeSSNConcept) - ssn: EmployeeSSN = new EmployeeSSNConcept(); // Automatically PII - - @field(EmployeeEmailConcept) - email: EmployeeEmail = new EmployeeEmailConcept(); // Automatically PII - - @field(String) - name: string = ''; -} -``` - -**Benefits of ConceptAs approach:** -- ✅ Mark PII classification once on the type -- ✅ Compliance metadata flows automatically from events to read models -- ✅ Type-safe with strong typing -- ✅ Proper serialization via JsonSerializer -- ✅ Self-documenting code - -3. **Register your read models** normally - the compliance metadata is automatically included in the schema sent to the Chronicle Kernel. - -4. **Release PII when needed**: - -```typescript -// Get instance with encrypted PII -const employee = await eventStore.readModels.getInstanceById(Employee, employeeId); - -// Release (decrypt) PII when authorized -const released = await eventStore.readModels.release(Employee, employee); -console.log(released.email); // Decrypted value -``` - -## Topics - -- [PII (Personal Identifiable Information)](./pii.md) - Learn about the `@pii` decorator and the recommended ConceptAs pattern -- [Read Models](./read-models.md) - Understand how compliance works with read models and how to use release functionality - -## Important Notes - -### Release Functionality - -The `release()` and `releaseMany()` methods are now available for decrypting PII properties when you have proper authorization. The Chronicle Kernel uses the `id` property of the read model instance as the subject for decryption, ensuring that you can only decrypt PII for the specific individual you're working with. - -### Best Practices - -1. **Use ConceptAs types** - Mark PII at the type level with ConceptAs for cross-cutting compliance -2. **Use @field decorators** - Ensure proper serialization with JsonSerializer by decorating all properties with `@field` -3. **Be explicit** - Always provide a details parameter explaining why a type or property is marked as PII -4. **Be minimal** - Only mark types or properties that actually contain PII -5. **Be consistent** - Use the same classification approach (preferably ConceptAs) across your entire application -6. **Review regularly** - Periodically audit your compliance annotations as your data model evolves -7. **Authorize releases** - Only call `release()` when you have proper authorization and a legitimate need to access decrypted PII +# Compliance -## Next Steps +Compliance and PII handling are shared Chronicle topics. The shared docs own the concepts, storage behavior, and client-tabbed examples. -- Learn about the [`@pii` decorator](./pii.md) -- Understand [compliance in read models](./read-models.md) +- [Compliance](/chronicle/compliance/) +- [PII](/chronicle/compliance/pii/) +- [Compliance in read models](/chronicle/compliance/read-models/) +- [TypeScript client setup](../getting-started.md) diff --git a/Documentation/compliance/pii.md b/Documentation/compliance/pii.md index 57f3c7e..21abfd7 100644 --- a/Documentation/compliance/pii.md +++ b/Documentation/compliance/pii.md @@ -1,485 +1,11 @@ -# PII (Personal Identifiable Information) +--- +sharedTopicBridge: true +--- -The `@pii` decorator is used to mark properties or types that contain Personal Identifiable Information according to the GDPR definition. When a property or type is decorated with `@pii`, the Chronicle Kernel automatically encrypts the value to ensure compliance with data protection regulations. +# PII -## Overview +PII classification and release behavior are shared Chronicle compliance topics. -Personal Identifiable Information (PII) is any data that could potentially identify a specific individual. Under GDPR, this data requires special protection and handling. - -The `@pii` decorator: -- Marks a property or type as containing PII -- Adds compliance metadata to the JSON schema -- Triggers automatic encryption by the Chronicle Kernel -- Can include optional details explaining the classification -- Works as both a property decorator and class decorator -- Enables cross-cutting compliance through type-level marking - -## Cross-Cutting Type-Level PII with ConceptAs (Recommended Pattern) - -The most powerful way to use `@pii` is at the type level with ConceptAs. When you mark a ConceptAs type as PII, **all properties of that type automatically inherit the PII classification** - no need to mark individual properties. - -This demonstrates the **cross-cutting nature** of the decorator: mark the type once, and the compliance metadata flows automatically from events to read models to queries. - -### Example: PII ConceptAs Types Flow from Events to Read Models - -```typescript -import { ConceptAs, field } from '@cratis/fundamentals'; -import { eventType, readModel, reducer, pii } from '@cratis/chronicle'; - -// Step 1: Define PII ConceptAs types (mark ONCE) -@pii('Customer email address') -class CustomerEmailConcept extends ConceptAs {} -export type CustomerEmail = CustomerEmailConcept | string; - -@pii('Customer full legal name') -class CustomerFullNameConcept extends ConceptAs {} -export type CustomerFullName = CustomerFullNameConcept | string; - -// Step 2: Use in events - PII metadata flows automatically -@eventType() -class CustomerRegistered { - @field(String) - customerId!: string; - - @field(CustomerEmailConcept) - email!: CustomerEmail; // Automatically PII - - @field(CustomerFullNameConcept) - fullName!: CustomerFullName; // Automatically PII -} - -// Step 3: Use in read models - same PII metadata flows automatically -@readModel() -class Customer { - @field(String) - id: string = ''; - - @field(CustomerEmailConcept) - email: CustomerEmail = new CustomerEmailConcept(); // Automatically PII - - @field(CustomerFullNameConcept) - fullName: CustomerFullName = new CustomerFullNameConcept(); // Automatically PII - - @field(String) - customerNumber: string = ''; // NOT PII -} - -// Step 4: Reducer just maps types - compliance handled automatically -@reducer('', undefined, Customer) -class CustomerReducer { - async customerRegistered(event: CustomerRegistered): Promise { - const customer = new Customer(); - customer.id = event.customerId; - customer.email = event.email; // PII metadata flows through - customer.fullName = event.fullName; // PII metadata flows through - customer.customerNumber = `CUST-${event.customerId}`; - return customer; - } -} -``` - -**Benefits:** -- ✅ Mark PII classification **once** on the ConceptAs type -- ✅ Compliance metadata flows **automatically** through events, read models, and queries -- ✅ No need to remember to mark individual properties -- ✅ Type-safe and refactoring-friendly with ConceptAs strong typing -- ✅ Proper serialization/deserialization via JsonSerializer and @field decorators -- ✅ Union type exports allow convenient primitive assignment - -**Why ConceptAs?** -- **Strong typing**: Prevents mixing up different domain concepts (email vs. phone number) -- **Proper serialization**: JsonSerializer handles ConceptAs types automatically via @field decorators -- **Domain-driven design**: Makes domain concepts explicit in the type system -- **Self-documenting**: Type names reflect ubiquitous language - -## Usage - -### Type-Level PII Marking with ConceptAs (Recommended) - -Apply the `@pii` decorator to ConceptAs types that represent PII: - -```typescript -import { ConceptAs, field } from '@cratis/fundamentals'; -import { pii, eventType, readModel } from '@cratis/chronicle'; - -// Define PII ConceptAs types -@pii('Customer email address') -class CustomerEmailConcept extends ConceptAs {} -export type CustomerEmail = CustomerEmailConcept | string; - -@pii('Customer full legal name') -class CustomerFullNameConcept extends ConceptAs {} -export type CustomerFullName = CustomerFullNameConcept | string; - -// Use in events and read models with @field decorators -@eventType() -class CustomerRegistered { - @field(String) - customerId!: string; - - @field(CustomerEmailConcept) - email!: CustomerEmail; // Automatically classified as PII - - @field(CustomerFullNameConcept) - fullName!: CustomerFullName; // Automatically classified as PII -} - -@readModel() -class Customer { - @field(String) - id: string = ''; - - @field(CustomerEmailConcept) - email: CustomerEmail = new CustomerEmailConcept(); // Automatically classified as PII - - @field(CustomerFullNameConcept) - fullName: CustomerFullName = new CustomerFullNameConcept(); // Automatically classified as PII -} -``` - -### Property-Level PII Marking (Legacy/Fallback) - -For cases where type-level marking isn't practical, you can mark individual properties: - -```typescript -import { readModel, pii } from '@cratis/chronicle'; - -@readModel() -class Employee { - id: string = ''; - - @pii('Employee social security number') - ssn: string = ''; - - @pii('Personal email address') - email: string = ''; - - name: string = ''; -} -``` - -**Note:** Property-level marking is less maintainable than type-level marking. If you find yourself marking the same property across multiple classes, consider creating a dedicated PII type instead. - -### Checking if a Type is PII - -You can programmatically check if a type has been marked as PII: - -```typescript -import { isPII, getTypePIIMetadata } from '@cratis/chronicle/compliance'; -import { CustomerEmailConcept } from './compliance-types'; - -// Check if a type is marked as PII -if (isPII(CustomerEmailConcept)) { - console.log('CustomerEmailConcept is PII'); -} - -// Get the PII metadata for a type -const metadata = getTypePIIMetadata(CustomerEmailConcept); -console.log(metadata?.details); // "Customer email address" -``` - -### Providing Details (Recommended) - -Always provide details explaining why a type or property is classified as PII: - -```typescript -import { ConceptAs } from '@cratis/fundamentals'; -import { pii } from '@cratis/chronicle'; - -// Type-level with details (recommended) -@pii('Customer email address for contact and notifications') -class CustomerEmailConcept extends ConceptAs {} -export type CustomerEmail = CustomerEmailConcept | string; - -@pii('Customer full legal name as registered') -class CustomerFullNameConcept extends ConceptAs {} -export type CustomerFullName = CustomerFullNameConcept | string; - -// Property-level with details (fallback for non-ConceptAs properties) -@readModel() -class Employee { - @field(String) - id: string = ''; - - @pii('Employee social security number for tax reporting') - @field(String) - ssn: string = ''; - - @pii('Personal email address for HR communications') - @field(String) - email: string = ''; -} -``` - -The details parameter serves as documentation and helps during compliance audits. - -## How PII Protection Works - -1. **Decoration**: You apply the `@pii` decorator to types or properties containing PII -2. **Schema Generation**: The compliance metadata is added to the JSON schema -3. **Type Propagation**: For type-level decorators, metadata flows to all properties of that type -4. **Transmission**: The schema is sent to the Chronicle Kernel during read model registration -5. **Encryption**: The Kernel encrypts PII property values before storing them -6. **Access**: Decryption only occurs when explicitly requested (future feature) - -## What Qualifies as PII? - -Under GDPR, PII includes (but is not limited to): - -### Direct Identifiers -- Full name -- Social security number -- Passport number -- Driver's license number -- Credit card number -- Email address (when it contains a real name) -- Telephone number - -### Indirect Identifiers -- IP address -- MAC address -- Device identifiers -- Cookie identifiers -- Location data -- Biometric data -- Health information - -### Special Categories -Some PII is considered "special category" and requires extra protection: -- Racial or ethnic origin -- Political opinions -- Religious or philosophical beliefs -- Trade union membership -- Genetic data -- Biometric data for identification -- Health data -- Data concerning sex life or sexual orientation - -## How PII Protection Works - -1. **Decoration**: You apply the `@pii` decorator to properties containing PII -2. **Schema Generation**: The compliance metadata is added to the JSON schema -3. **Transmission**: The schema is sent to the Chronicle Kernel during read model registration -4. **Encryption**: The Kernel encrypts PII property values before storing them -5. **Access**: Decryption only occurs when explicitly requested (future feature) - -## Examples - -### User Profile (Type-Level PII with ConceptAs) - -```typescript -import { ConceptAs, field } from '@cratis/fundamentals'; -import { eventType, readModel, reducer, pii } from '@cratis/chronicle'; - -// Define PII ConceptAs types -@pii('User full name') -class UserFullNameConcept extends ConceptAs {} -export type UserFullName = UserFullNameConcept | string; - -@pii('Primary contact email') -class UserEmailConcept extends ConceptAs {} -export type UserEmail = UserEmailConcept | string; - -@pii('Primary contact phone') -class UserPhoneConcept extends ConceptAs {} -export type UserPhone = UserPhoneConcept | string; - -@pii('Home address') -class UserAddressConcept extends ConceptAs {} -export type UserAddress = UserAddressConcept | string; - -// Event with PII ConceptAs types -@eventType() -class UserRegistered { - @field(String) - userId!: string; - - @field(UserFullNameConcept) - fullName!: UserFullName; - - @field(UserEmailConcept) - email!: UserEmail; - - @field(UserPhoneConcept) - phone!: UserPhone; -} - -// Read model with same PII ConceptAs types -@readModel() -class UserProfile { - @field(String) - id: string = ''; - - @field(UserFullNameConcept) - fullName: UserFullName = new UserFullNameConcept(); // Automatically PII - - @field(UserEmailConcept) - email: UserEmail = new UserEmailConcept(); // Automatically PII - - @field(UserPhoneConcept) - phone: UserPhone = new UserPhoneConcept(); // Automatically PII - - @field(UserAddressConcept) - address: UserAddress = new UserAddressConcept(); // Automatically PII - - // Not PII - public username - @field(String) - username: string = ''; - - // Not PII - preference setting - @field(String) - theme: string = ''; -} - -// Reducer - PII metadata flows through ConceptAs types -@reducer('', undefined, UserProfile) -class UserProfileReducer { - async userRegistered(event: UserRegistered): Promise { - const profile = new UserProfile(); - profile.id = event.userId; - profile.fullName = event.fullName; // PII flows through - profile.email = event.email; // PII flows through - profile.phone = event.phone; // PII flows through - profile.username = event.userId; - profile.theme = 'light'; - return profile; - } -} -``` - -### Healthcare Record (Type-Level PII with ConceptAs) - -```typescript -import { ConceptAs, field } from '@cratis/fundamentals'; -import { eventType, readModel, reducer, pii } from '@cratis/chronicle'; - -// Define PII ConceptAs types for healthcare -@pii('Patient full name') -class PatientNameConcept extends ConceptAs {} -export type PatientName = PatientNameConcept | string; - -@pii('Date of birth') -class DateOfBirthConcept extends ConceptAs {} -export type DateOfBirth = DateOfBirthConcept | string; - -@pii('Medical record number') -class MedicalRecordNumberConcept extends ConceptAs {} -export type MedicalRecordNumber = MedicalRecordNumberConcept | string; - -@pii('Diagnosis information') -class DiagnosisInfoConcept extends ConceptAs {} -export type DiagnosisInfo = DiagnosisInfoConcept | string; - -// Event with PII ConceptAs types -@eventType() -class PatientAdmitted { - @field(String) - patientId!: string; - - @field(PatientNameConcept) - patientName!: PatientName; - - @field(DateOfBirthConcept) - dateOfBirth!: DateOfBirth; - - @field(MedicalRecordNumberConcept) - medicalRecordNumber!: MedicalRecordNumber; -} - -// Read model with same PII ConceptAs types -@readModel() -class PatientRecord { - @field(String) - id: string = ''; - - @field(PatientNameConcept) - patientName: PatientName = new PatientNameConcept(); // Automatically PII - - @field(DateOfBirthConcept) - dateOfBirth: DateOfBirth = new DateOfBirthConcept(); // Automatically PII - - @field(MedicalRecordNumberConcept) - medicalRecordNumber: MedicalRecordNumber = new MedicalRecordNumberConcept(); // Automatically PII - - @field(DiagnosisInfoConcept) - diagnosis: DiagnosisInfo = new DiagnosisInfoConcept(); // Automatically PII - - // Not PII - anonymized statistics - @field(Number) - visitCount: number = 0; -} -``` - -### Property-Level PII (Fallback for Simple Cases) - -For simple cases where creating types feels like overkill, you can use property-level marking: - -```typescript -import { eventType } from '@cratis/chronicle'; - -@eventType() -class SimpleUserRegistered { - constructor( - public userId: string, - @pii('User email address') public email: string, - @pii('User full name') public fullName: string, - public username: string // Not PII - ) {} -} -``` - -**Note:** While you can use `@pii` on events (either type-level or property-level), remember that events are immutable. The decorator ensures the PII is marked in schemas, but consider carefully whether PII belongs in your event stream. Using type-level PII allows you to apply the same types across events and read models consistently. - -## Best Practices - -### DO: -- ✅ **Use type-level `@pii` on ConceptAs types for cross-cutting compliance** - mark ConceptAs types once, use everywhere -- ✅ **Create dedicated PII ConceptAs types** for common PII patterns (Email, FullName, PhoneNumber, etc.) -- ✅ **Reuse PII ConceptAs types** across events and read models for consistency -- ✅ **Use @field decorators** to ensure proper JsonSerializer serialization/deserialization -- ✅ **Export ConceptAs as union types** (e.g., `export type Email = EmailConcept | string`) for convenience -- ✅ **Provide clear details** explaining why each type is classified as PII -- ✅ **Review PII classifications** during code reviews -- ✅ **Document PII classification decisions** for compliance audits -- ✅ **Be conservative** - when in doubt, mark it as PII - -### DON'T: -- ❌ **Don't repeat property-level `@pii` everywhere** - use type-level ConceptAs instead -- ❌ **Don't forget @field decorators** on ConceptAs properties (serialization won't work properly) -- ❌ **Don't mark non-PII properties** just to be safe (this adds unnecessary overhead) -- ❌ **Don't forget to update PII markings** when your data model changes -- ❌ **Don't use PII data** for non-essential features -- ❌ **Don't log PII data** without proper safeguards -- ❌ **Don't assume encrypted PII is "safe"** to share widely - -## Compliance Considerations - -### Data Minimization -Only collect and store PII that is necessary for your application's functionality. Don't mark something as PII if you don't need to collect it in the first place. - -### Purpose Limitation -Ensure that PII is only used for the purposes disclosed to the data subject. The details parameter can help document the intended use. - -### Storage Limitation -PII should not be kept longer than necessary. Consider implementing data retention policies in conjunction with compliance annotations. - -### Right to Erasure -GDPR grants individuals the "right to be forgotten". Chronicle's compliance features support this through: -- Encryption key deletion (making data unrecoverable) -- Event tombstoning for event streams -- Read model session management - -## Future Features - -The following features are planned for future releases: - -- **Release/Decryption**: Methods to decrypt PII properties when needed -- **Subject-based Access**: Decrypt PII based on data subject identity -- **Audit Logging**: Track who accessed PII and when -- **Retention Policies**: Automatic expiration of PII -- **Additional Compliance Types**: Support for other compliance requirements beyond PII - -## See Also - -- [Compliance Overview](./index.md) -- [Read Models](./read-models.md) +- [PII](/chronicle/compliance/pii/) +- [Compliance](/chronicle/compliance/) +- [TypeScript client setup](../getting-started.md) diff --git a/Documentation/compliance/read-models.md b/Documentation/compliance/read-models.md index 8c14117..22a6dbf 100644 --- a/Documentation/compliance/read-models.md +++ b/Documentation/compliance/read-models.md @@ -1,451 +1,11 @@ -# Compliance in Read Models - -Read models in Chronicle TypeScript fully support compliance features. When you decorate read model properties with `@pii` or other compliance decorators, the compliance metadata is automatically included in the read model schema and enforced by the Chronicle Kernel. - -## Overview - -Read models are projections of your event stream, and they often contain Personal Identifiable Information (PII) or other sensitive data. Chronicle's compliance support ensures that: - -1. **PII is marked** in the read model schema -2. **Encryption is automatic** when the Kernel stores the read model -3. **Decryption is controlled** through the release mechanism (coming soon) -4. **Compliance is transparent** to your application logic - -## Decorating Read Models with ConceptAs (Recommended) - -The best way to add compliance to read models is to use ConceptAs types marked with `@pii` at the type level. This demonstrates the cross-cutting nature of compliance where PII metadata flows automatically from events to read models: - -```typescript -import { ConceptAs, field } from '@cratis/fundamentals'; -import { readModel, pii } from '@cratis/chronicle'; - -// Define PII ConceptAs types once -@pii('Employee social security number') -class EmployeeSSNConcept extends ConceptAs {} -export type EmployeeSSN = EmployeeSSNConcept | string; - -@pii('Personal email address') -class EmployeeEmailConcept extends ConceptAs {} -export type EmployeeEmail = EmployeeEmailConcept | string; - -@pii('Home address') -class EmployeeAddressConcept extends ConceptAs {} -export type EmployeeAddress = EmployeeAddressConcept | string; - -// Use in read model - PII metadata flows automatically -@readModel() -class Employee { - @field(String) - id: string = ''; - - @field(EmployeeSSNConcept) - ssn: EmployeeSSN = new EmployeeSSNConcept(); // Automatically PII - - @field(EmployeeEmailConcept) - email: EmployeeEmail = new EmployeeEmailConcept(); // Automatically PII - - @field(EmployeeAddressConcept) - address: EmployeeAddress = new EmployeeAddressConcept(); // Automatically PII - - // Not PII - @field(String) - employeeNumber: string = ''; - - @field(String) - department: string = ''; -} -``` - -Benefits of the ConceptAs approach: -- Mark PII classification once on the ConceptAs type -- Compliance metadata flows automatically from events to read models -- Type-safe with strong typing -- Proper serialization via JsonSerializer with @field decorators - -### Property-Level Marking (Fallback) - -For simple cases where creating ConceptAs types isn't practical, you can mark properties directly: - -```typescript -import { readModel, pii } from '@cratis/chronicle'; - -@readModel() -class Employee { - id: string = ''; - - @pii('Employee social security number') - ssn: string = ''; - - @pii('Personal email address') - email: string = ''; - - @pii('Home address') - address: string = ''; - - // Not PII - employeeNumber: string = ''; - department: string = ''; -} -``` - -When this read model is registered with the Chronicle Kernel, the schema includes compliance metadata for the `ssn`, `email`, and `address` properties. - -## How It Works - -### Schema Generation - -When you register a read model, the `JsonSchemaGenerator` processes your class definition: - -1. **Discovers decorators**: It finds all `@pii` and other compliance decorators -2. **Collects metadata**: It retrieves the compliance metadata (type and details) -3. **Adds to schema**: It adds a `compliance` array to each property's schema - -The resulting JSON schema includes compliance information: - -```json -{ - "type": "object", - "properties": { - "ssn": { - "type": "string", - "compliance": [ - { - "metadataType": "cae5580e-83d6-44dc-9d7a-a72e8a2f17d7", - "details": "Employee social security number" - } - ] - } - } -} -``` - -The `metadataType` GUID identifies the type of compliance (in this case, PII). - -### Automatic Encryption - -When the Chronicle Kernel receives the read model schema: - -1. **Parses compliance metadata**: The Kernel identifies which properties need encryption -2. **Encrypts on write**: When a read model instance is stored, PII properties are encrypted -3. **Stores encrypted**: The encrypted values are stored in MongoDB -4. **Returns encrypted**: When you query the read model, you receive the encrypted values - -This encryption is transparent to your application code - you work with plain objects, and the Kernel handles encryption/decryption. - -## Release (Decryption) - -Chronicle TypeScript now provides full support for releasing (decrypting) PII properties when you have a legitimate need to access them. This is a key part of GDPR compliance: PII should only be decrypted when necessary and authorized. - -### API - -The `release()` and `releaseMany()` methods are available on `IReadModels`: - -```typescript -// Get an employee with encrypted PII -const employee = await eventStore.readModels.getInstanceById(Employee, employeeId); -console.log(employee.ssn); // Encrypted value - -// Release (decrypt) the PII for this employee -const released = await eventStore.readModels.release(Employee, employee); -console.log(released.ssn); // Decrypted value - -// Release multiple instances at once -const employees = await eventStore.readModels.getInstances(Employee); -const releasedEmployees = await eventStore.readModels.releaseMany(Employee, employees); -``` - -### Subject-based Decryption - -The release mechanism uses the concept of a "subject" - the individual whose PII is being decrypted. By convention, Chronicle uses the `id` property as the subject identifier. - -When you call `release()`, Chronicle: -1. Extracts the subject from the instance's `id` property -2. Sends a Release request to the Compliance service with the subject -3. The Kernel decrypts only the PII for that specific subject -4. Returns a new instance with decrypted values - -This ensures that you can only decrypt PII for the specific individual you're working with, not all PII in your system. - -### Implementation Details - -The `release()` method: -- Serializes the read model instance using JsonSerializer -- Sends the schema and payload to the Compliance service -- Uses the instance's `id` property as the subject -- Returns a deserialized instance with decrypted PII values -- Throws an error if the release operation fails - -The `releaseMany()` method calls `release()` for each instance in parallel. - -## Examples - -### Basic Read Model with PII using ConceptAs - -```typescript -import { ConceptAs, field } from '@cratis/fundamentals'; -import { readModel, pii } from '@cratis/chronicle'; - -// Define PII ConceptAs types -@pii('Customer full name') -class CustomerFullNameConcept extends ConceptAs {} -export type CustomerFullName = CustomerFullNameConcept | string; - -@pii('Primary email') -class CustomerEmailConcept extends ConceptAs {} -export type CustomerEmail = CustomerEmailConcept | string; +--- +sharedTopicBridge: true +--- -@pii('Billing address') -class CustomerBillingAddressConcept extends ConceptAs {} -export type CustomerBillingAddress = CustomerBillingAddressConcept | string; - -@pii('Phone number') -class CustomerPhoneConcept extends ConceptAs {} -export type CustomerPhone = CustomerPhoneConcept | string; - -// Use in read model -@readModel() -class Customer { - @field(String) - id: string = ''; - - @field(CustomerFullNameConcept) - fullName: CustomerFullName = new CustomerFullNameConcept(); - - @field(CustomerEmailConcept) - email: CustomerEmail = new CustomerEmailConcept(); - - @field(CustomerBillingAddressConcept) - billingAddress: CustomerBillingAddress = new CustomerBillingAddressConcept(); - - @field(CustomerPhoneConcept) - phone: CustomerPhone = new CustomerPhoneConcept(); - - // Not PII - @field(String) - customerNumber: string = ''; - - @field(String) - accountStatus: string = ''; - - @field(Number) - totalOrders: number = 0; -} - -// Usage -const customer = await eventStore.readModels.getInstanceById(Customer, customerId); -// customer.email contains encrypted value - -// Decrypt when needed -const released = await eventStore.readModels.release(Customer, customer); -// released.email contains decrypted value -``` - -### Projection with PII - -When building projections that populate read models with PII, the projection logic remains unchanged: - -```typescript -import { - readModel, - projection, - fromEvent, - pii -} from '@cratis/chronicle'; - -@eventType() -class CustomerRegistered { - constructor( - public customerId: string, - public email: string, - public fullName: string - ) {} -} - -@eventType() -class CustomerAddressUpdated { - constructor( - public customerId: string, - public address: string - ) {} -} - -@readModel() -@projection() -class Customer { - id: string = ''; - - @pii('Customer email') - @fromEvent(CustomerRegistered, (event, model) => model.email = event.email) - email: string = ''; - - @pii('Customer full name') - @fromEvent(CustomerRegistered, (event, model) => model.fullName = event.fullName) - fullName: string = ''; - - @pii('Customer address') - @fromEvent(CustomerAddressUpdated, (event, model) => model.address = event.address) - address: string = ''; - - customerNumber: string = ''; -} -``` - -The projection decorators work normally - the PII metadata is separate and doesn't affect projection behavior. - -### Reducer with PII - -Similarly, reducers work unchanged with PII-marked properties: - -```typescript -import { readModel, reducer, on, pii } from '@cratis/chronicle'; - -@readModel() -@reducer() -class UserProfile { - id: string = ''; - - @pii('User full name') - fullName: string = ''; - - @pii('User email') - email: string = ''; - - username: string = ''; - - @on(UserRegistered) - registered(event: UserRegistered) { - this.id = event.userId; - this.fullName = event.fullName; - this.email = event.email; - this.username = event.username; - } - - @on(UserEmailChanged) - emailChanged(event: UserEmailChanged) { - this.email = event.newEmail; - } -} -``` - -## Read Model Sessions - -Chronicle read models support sessions, which can be used in conjunction with compliance features: - -```typescript -// Create a session for working with PII -const sessionId = 'audit-session-123'; -const employee = await eventStore.readModels - .getInstanceById(Employee, employeeId, sessionId); - -// Future: Release PII within the session -// const released = await eventStore.readModels.release(Employee, employee); - -// Dehydrate (cleanup) the session when done -await eventStore.readModels.dehydrateSession(sessionId); -``` - -Sessions allow you to: -- Track access to PII -- Manage temporary decrypted state -- Implement audit logging -- Control data lifetime in memory - -## Best Practices - -### DO: -- ✅ Mark all PII properties with `@pii` in read models -- ✅ Use sessions when working with decrypted PII -- ✅ Only decrypt PII when necessary for your use case -- ✅ Dehydrate sessions promptly after use -- ✅ Document why each property is marked as PII - -### DON'T: -- ❌ Log decrypted PII values -- ❌ Store decrypted PII in client-side storage -- ❌ Pass decrypted PII to third-party services without consent -- ❌ Keep sessions with decrypted PII open longer than necessary -- ❌ Decrypt PII for display purposes without user authorization - -## Architecture - -### Schema Integration - -The compliance metadata flows through the system like this: - -``` -@pii decorator → TypeIntrospector → JsonSchemaGenerator → Chronicle Kernel -``` - -1. `@pii` decorator stores metadata using reflect-metadata -2. `TypeIntrospector` tracks the property for schema generation -3. `JsonSchemaGenerator` collects compliance metadata and adds it to the schema -4. The Kernel receives the schema and configures encryption - -### Metadata Format - -Compliance metadata in the schema follows this structure: - -```typescript -{ - metadataType: string, // GUID identifying the compliance type (e.g., PII) - details: string // Human-readable explanation -} -``` - -This format matches the C# implementation, ensuring consistency across Chronicle clients. - -### Type Safety - -The compliance decorators are type-safe and work with TypeScript's type system: - -```typescript -@readModel() -class Example { - @pii('Sensitive data') - sensitiveField: string = ''; // ✅ Works with any type - - @pii('Numeric PII') - numericField: number = 0; // ✅ Works with numbers - - @pii('Complex PII') - complexField: Address = new Address(); // ✅ Works with objects -} -``` - -## Troubleshooting - -### Metadata Not Appearing in Schema - -If compliance metadata isn't appearing in your generated schemas: - -1. Ensure you've imported the decorator: `import { pii } from '@cratis/chronicle/compliance';` -2. Verify the decorator is applied correctly: `@pii('details')` -3. Check that `reflect-metadata` is imported at your application entry point -4. Ensure the read model class is decorated with `@readModel()` - -### Encryption Not Applied - -If the Kernel isn't encrypting PII properties: - -1. Verify the compliance metadata is in the schema (check Kernel logs) -2. Ensure you're using a recent version of the Chronicle Kernel that supports compliance -3. Check that the property type is supported by the encryption system - -## Future Enhancements - -Planned features for read model compliance: - -- **Release API**: Full implementation of `release()` and `releaseMany()` -- **Granular decryption**: Decrypt specific properties, not entire instances -- **Access control**: Integration with Chronicle's authorization system -- **Audit trail**: Automatic logging of PII access -- **Retention policies**: Automatic cleanup of PII based on retention rules -- **Anonymization**: Automatic anonymization of expired PII +# Compliance in Read Models -## See Also +Read-model compliance is documented in the shared Chronicle compliance section so all clients use the same rules. -- [Compliance Overview](./index.md) -- [PII Decorator](./pii.md) -- [Read Models Guide](../read-models.md) -- [Projections Guide](../projections.md) -- [Reducers Guide](../reducers.md) +- [Compliance in read models](/chronicle/compliance/read-models/) +- [Read models](/chronicle/read-models/) +- [TypeScript client setup](../getting-started.md) diff --git a/Documentation/correlation.md b/Documentation/correlation.md index b60d283..8568e1a 100644 --- a/Documentation/correlation.md +++ b/Documentation/correlation.md @@ -1,6 +1,6 @@ # Correlation -A correlation identifier (`CorrelationId`) ties together all events, commands, and side effects that belong to the same logical operation — for example, a single HTTP request or a message-bus handler invocation. It is sent with every event append as the `CorrelationId` field. +See [Correlation, identity, and causation](/chronicle/concepts/correlation-identity-causation/) for what a correlation ID is and why Chronicle tracks it. This page covers the TypeScript-specific API in depth. A `CorrelationId` is sent with every event append as the `CorrelationId` field. ## `CorrelationId` diff --git a/Documentation/event-log.md b/Documentation/event-log.md index 82622a2..548fd43 100644 --- a/Documentation/event-log.md +++ b/Documentation/event-log.md @@ -1,134 +1,13 @@ -# Event Log - -The event log is the primary event sequence in an event store. All domain events are appended to it. - -## Appending Events - -```typescript -import { ChronicleClient, ChronicleOptions, eventType } from '@cratis/chronicle'; - -@eventType() -class EmployeeHired { - constructor(readonly firstName: string, readonly lastName: string) {} -} - -const client = new ChronicleClient(ChronicleOptions.development()); -const store = await client.getEventStore('MyStore'); - -const result = await store.eventLog.append('employee-123', new EmployeeHired('Jane', 'Doe')); - -if (result.isSuccess) { - console.log(`Event appended at sequence number ${result.sequenceNumber.value}`); -} else { - console.error('Append failed:', result.errors); -} -``` - -## Appending Multiple Events - -```typescript -const results = await store.eventLog.appendMany('employee-123', [ - new EmployeeHired('Jane', 'Doe'), - new EmployeePromoted('Senior Engineer') -]); -``` - -## Using Concurrency Scope - -Use `AppendOptions.concurrencyScope` when you want Chronicle to validate expected sequence state before committing events. -Use `eventSourceId: true` when you want validation against events already appended for the same `eventSourceId` before this operation. -Set `eventSourceType` only if you partition event sources by your own named types (for example `Employee`, `Order`, `Invoice`). Omit it when you use Chronicle's default source partitioning. -Use `eventStreamType` + `eventStreamId` when you need concurrency validation against one stream within that source. -Set `sequenceNumber` to the last known sequence number that must already exist before Chronicle accepts the append. -Set `eventTypes` when you want concurrency validation to consider only specific event types. -The sample below shows one end-to-end flow using source-level and stream-level concurrency scopes. -`getTailSequenceNumber()` always returns an `EventSequenceNumber`, so you pass its `.value` (a `bigint`) to `sequenceNumber`. - -```typescript -import { ChronicleClient, ChronicleOptions, eventType, getEventTypeFor } from '@cratis/chronicle'; - -@eventType() -class EmployeeHired { - constructor(readonly firstName: string, readonly lastName: string) {} -} - -@eventType() -class EmployeePromoted { - constructor(readonly title: string) {} -} +--- +sharedTopicBridge: true +--- -@eventType() -class EmployeeDepartmentChanged { - constructor(readonly department: string) {} -} - -async function appendWithConcurrencyScopes() { - const client = new ChronicleClient(ChronicleOptions.development()); - const store = await client.getEventStore('MyStore'); - const eventSourceId = 'employee-123'; - const expectedSequenceNumber = (await store.eventLog.getTailSequenceNumber(eventSourceId)).value; - - // Scope concurrency to this event source. - const appendResult = await store.eventLog.append(eventSourceId, new EmployeeHired('Jane', 'Doe'), { - concurrencyScope: { - sequenceNumber: expectedSequenceNumber, - // true means "use the eventSourceId parameter passed to append()". - eventSourceId: true - } - }); - - if (!appendResult.isSuccess) { - console.error(appendResult.errors, appendResult.constraintViolations); - return; - } - - const expectedSequenceNumberForBatch = (await store.eventLog.getTailSequenceNumber(eventSourceId)).value; - - // Combine source + stream fields when your boundary is a specific stream in a specific source. - const appendManyResults = await store.eventLog.appendMany(eventSourceId, [ - new EmployeePromoted('Senior Engineer'), - new EmployeeDepartmentChanged('Platform') - ], { - concurrencyScope: { - sequenceNumber: expectedSequenceNumberForBatch, - eventSourceId: true, - eventSourceType: 'Employee', // "Employee" is a custom source type name (not Chronicle's default source type). - eventStreamType: 'Career', - eventStreamId: `${eventSourceId}-career`, - eventTypes: [getEventTypeFor(EmployeePromoted), getEventTypeFor(EmployeeDepartmentChanged)] - } - }); - - if (appendManyResults.some(_ => !_.isSuccess)) { - console.error(appendManyResults); - } -} - -await appendWithConcurrencyScopes(); -``` - -## Checking for Events - -```typescript -const hasEvents = await store.eventLog.hasEventsFor('employee-123'); -``` - -## Getting the Tail Sequence Number - -The tail sequence number is the sequence number of the most recently appended event: - -```typescript -const tailSequenceNumber = await store.eventLog.getTailSequenceNumber('employee-123'); -console.log(`Latest event at: ${tailSequenceNumber.value}`); -``` - -## AppendResult +# Event Log -Every append returns an `AppendResult`: +The event log is Chronicle's primary event sequence and is documented in the shared Chronicle docs. -| Property | Description | -|----------|-------------| -| `sequenceNumber` | The sequence number assigned to the appended event. | -| `isSuccess` | `true` if there were no errors or constraint violations. | -| `errors` | Array of error messages, if any. | -| `constraintViolations` | Array of constraint violations, if any. | +- [Events and event logs](/chronicle/events/) +- [Appending events](/chronicle/events/appending/) +- [Appending many events](/chronicle/events/appending-many/) +- [Optimistic concurrency](/chronicle/events/concurrency/) +- [TypeScript client setup](./getting-started.md) diff --git a/Documentation/event-type-migrations.md b/Documentation/event-type-migrations.md index 3885d8b..bff998f 100644 --- a/Documentation/event-type-migrations.md +++ b/Documentation/event-type-migrations.md @@ -1,71 +1,12 @@ -# Event Type Migrations - -Event type migrations let you define explicit upcast and downcast rules between adjacent generations of the same event type. - -Use migrations when an event schema changes and you need Chronicle to understand how to move event payloads between generations. - -## Define Event Generations - -Start by defining two generations of the same event type id: - -```typescript -import { eventType } from '@cratis/chronicle'; - -@eventType('EmployeeRegistered', 1) -class EmployeeRegisteredV1 { - constructor(readonly fullName: string) {} -} - -@eventType('EmployeeRegistered', 2) -class EmployeeRegisteredV2 { - constructor(readonly firstName: string, readonly lastName: string) {} -} -``` - -## Define a Migration +--- +sharedTopicBridge: true +--- -Use `@eventTypeMigration` to register a discoverable migration class: - -```typescript -import { - eventTypeMigration, - IEventTypeMigration, - IEventMigrationBuilder -} from '@cratis/chronicle'; - -@eventTypeMigration(EmployeeRegisteredV2, EmployeeRegisteredV1) -class EmployeeRegisteredMigration - implements IEventTypeMigration { - upcast(builder: IEventMigrationBuilder): void { - builder.properties(properties => { - properties - .split('firstName', 'fullName', ' ', 0) - .split('lastName', 'fullName', ' ', 1); - }); - } - - downcast(builder: IEventMigrationBuilder): void { - builder.properties(properties => { - properties.combine('fullName', ' ', 'firstName', 'lastName'); - }); - } -} -``` - -## Property Migration Operations - -Inside `builder.properties(...)`, you can configure: - -- `split(targetProperty, sourceProperty, separator, part)` -- `combine(targetProperty, separator, ...sourceProperties)` -- `renamedFrom(targetProperty, sourceProperty)` -- `defaultValue(targetProperty, value)` - -## Rules +# Event Type Migrations -- Both event classes must be decorated with `@eventType(...)`. -- Both generations must have the same event type id. -- The upgraded generation must be exactly one greater than the previous generation. -- The migration class must be loaded by your application so decorator discovery can register it. +Event type migrations are shared Chronicle behavior. The shared documentation owns the migration model and client-tabbed examples. -If a migration violates generation adjacency, Chronicle throws `InvalidMigrationGenerationGap`. +- [Event type migrations concept](/chronicle/concepts/event-type-migrations/) +- [Understanding event evolution](/chronicle/understanding-event-evolution/) +- [Migrations](/chronicle/migrations/) +- [TypeScript client setup](./getting-started.md) diff --git a/Documentation/event-types.md b/Documentation/event-types.md index e497eeb..8cee7d0 100644 --- a/Documentation/event-types.md +++ b/Documentation/event-types.md @@ -1,51 +1,12 @@ -# Event Types - -Event types are the domain objects that represent things that happened in your system. They are immutable records of facts. - -## Defining an Event Type - -Use the `@eventType` decorator to mark a class as an event type: - -```typescript -import { eventType } from '@cratis/chronicle'; - -@eventType() -class EmployeeHired { - constructor( - readonly firstName: string, - readonly lastName: string, - readonly title: string - ) {} -} -``` - -By default, Chronicle uses the class name as the event type id, generation `1`, and tombstone `false`. +--- +sharedTopicBridge: true +--- -You can optionally provide generation and tombstone: - -```typescript -@eventType(2) -class EmployeeHiredV2 {} - -@eventType(true) -class EmployeeRetired {} -``` - -## Rules for Event Types - -- Events are **immutable facts** — never add nullable properties; instead, create a new event type. -- Keep class names stable because the default id comes from the class name. -- Increase generation when the schema changes. -- Event type classes should only contain constructor parameters; no business logic. - -## Accessing Event Type Metadata - -You can retrieve the event type metadata for a class at runtime: +# Event Types -```typescript -import { getEventTypeFor } from '@cratis/chronicle'; +Event types are a shared Chronicle concept. Use the shared documentation for the model, naming rules, generations, and schema evolution flow. -const eventTypeInfo = getEventTypeFor(EmployeeHired); -console.log(eventTypeInfo.id.value); // 'EmployeeHired' -console.log(eventTypeInfo.generation.value); // 1 -``` +- [Event type concept](/chronicle/concepts/event-type/) +- [Events and event logs](/chronicle/events/) +- [Event evolution](/chronicle/understanding-event-evolution/) +- [TypeScript client setup](./getting-started.md) diff --git a/Documentation/getting-started.md b/Documentation/getting-started.md index 7a477ae..5061248 100644 --- a/Documentation/getting-started.md +++ b/Documentation/getting-started.md @@ -63,3 +63,11 @@ const options = ChronicleOptions.development(); ``` This connects to `chronicle://localhost:35000` by default. + +## Where to next + +- [Events and event logs](event-log.md), [Event Types](event-types.md), and [Event Evolution](event-type-migrations.md) to model and evolve your event schema. +- [Reducers](reducers.md), [Reactors](reactors.md), and [Projections](projections.md) to build read models and side effects from your events. +- [Read Models](read-models.md) to query projected state. +- [Event Seeding](seeding.md) and [Transactions](transactions.md) for test data and unit-of-work semantics. +- [Compliance](compliance/index.md) for PII handling and GDPR erasure. diff --git a/Documentation/identity.md b/Documentation/identity.md index fbf4711..390db7a 100644 --- a/Documentation/identity.md +++ b/Documentation/identity.md @@ -1,6 +1,6 @@ # Identity -Every state change in Chronicle is tied to an *identity* — the user, service, or system that caused it. The `Identity` class captures that information and is sent with every event append as the `CausedBy` field. +See [Correlation, identity, and causation](/chronicle/concepts/correlation-identity-causation/) for what identity means and why Chronicle tracks it. This page covers the TypeScript-specific API in depth. The `Identity` class is sent with every event append as the `CausedBy` field. ## The `Identity` class diff --git a/Documentation/index.md b/Documentation/index.md index f547b97..8a1a58c 100644 --- a/Documentation/index.md +++ b/Documentation/index.md @@ -1,30 +1,28 @@ -# Chronicle TypeScript Client Documentation +# Chronicle TypeScript Client -Welcome to the Chronicle TypeScript client documentation. +`@cratis/chronicle` is the TypeScript SDK for Chronicle. It builds on top of the [`@cratis/chronicle.contracts`](https://www.npmjs.com/package/@cratis/chronicle.contracts) gRPC contracts package and provides TypeScript APIs for event stores, event logs, observers, read models, jobs, sinks, and metadata. -## Overview +Use this section for TypeScript installation, connection setup, decorators, generated APIs, and runtime integration details. Shared Chronicle concepts and workflows live in the main Chronicle docs and use language tabs when code differs by client. -`@cratis/chronicle` is a TypeScript-idiomatic client for the Cratis Chronicle event-sourcing kernel. It builds on top of the [`@cratis/chronicle.contracts`](https://www.npmjs.com/package/@cratis/chronicle.contracts) gRPC contracts package and provides a clean, type-safe API for: +## Shared Chronicle topics -- Appending events to event sequences -- Managing event stores and namespaces -- Defining reactors, reducers, seeders, projections, constraints, and model-bound read models using TypeScript decorators such as `@readModel` and `@fromEvent` +- [Get started](/chronicle/get-started/) +- [Events and event logs](/chronicle/events/) +- [Appending events](/chronicle/events/appending/) +- [Read models](/chronicle/read-models/) +- [Projections](/chronicle/projections/) +- [Reactors](/chronicle/reactors/) +- [Reducers](/chronicle/reducers/) +- [Constraints](/chronicle/constraints/) +- [Event seeding](/chronicle/event-seeding/) +- [Compliance](/chronicle/compliance/) +- [Transactions and unit of work](/chronicle/events/transactions/) +- [Event evolution](/chronicle/understanding-event-evolution/) -## Guides +## TypeScript-specific pages -- [Getting Started](./getting-started.md) — Install and connect to Chronicle -- [Event Types](./event-types.md) — Defining event types with the `@eventType` decorator -- [Event Type Migrations](./event-type-migrations.md) — Defining generation migrations with `@eventTypeMigration` -- [Event Log](./event-log.md) — Appending events and querying the event log -- [Reactors](./reactors.md) — Reacting to events with the `@reactor` decorator -- [Reducers](./reducers.md) — Folding events into state with the `@reducer` decorator -- [Read Models](./read-models.md) — Querying and watching read model instances from an event store -- [Sinks](./sinks.md) — Configuring where read models are persisted -- [Jobs](./jobs.md) — Managing Chronicle jobs from an event store -- [Webhooks](./webhooks.md) — Registering and discovering client webhooks -- [Seeding](./seeding.md) — Seeding initial events with the `@seeder` decorator -- [Identity](./identity.md) — Tracking who caused a state change -- [Auditing — Causation](./auditing.md) — Recording the causation chain for events -- [Correlation](./correlation.md) — Correlating events and operations with a shared identifier -- [Transactions (Unit of Work)](./transactions.md) — Group appends and commit or roll back as one unit -- [Compliance](./compliance/index.md) — Protecting Personal Identifiable Information (PII) and meeting regulatory requirements +- [Getting Started](./getting-started.md) — install and connect the TypeScript client +- [Sinks](./sinks.md) — configuring read-model persistence targets +- [Jobs](./jobs.md) — managing Chronicle jobs from TypeScript +- [Webhooks](./webhooks.md) — registering and discovering client webhooks +- [Identity](./identity.md), [Causation](./auditing.md), and [Correlation](./correlation.md) — TypeScript call-context metadata diff --git a/Documentation/jobs.md b/Documentation/jobs.md index 36686ff..236620e 100644 --- a/Documentation/jobs.md +++ b/Documentation/jobs.md @@ -1,6 +1,6 @@ # Jobs -Use `eventStore.jobs` to inspect and control Chronicle jobs in a namespace. +See [Jobs](/chronicle/jobs/) for what a job is. Use `eventStore.jobs` to inspect and control Chronicle jobs in a namespace. ## API diff --git a/Documentation/projections.md b/Documentation/projections.md new file mode 100644 index 0000000..f6ea023 --- /dev/null +++ b/Documentation/projections.md @@ -0,0 +1,13 @@ +--- +sharedTopicBridge: true +--- + +# Projections + +Projections are shared Chronicle read-model behavior. Use the shared docs for projection styles, model-bound projections, declarative projections, and client-tabbed examples. + +- [Projections](/chronicle/projections/) +- [Choosing a read model style](/chronicle/projections/choosing-a-read-model-style/) +- [Model-bound projections](/chronicle/projections/model-bound/) +- [Declarative projections](/chronicle/projections/declarative/) +- [TypeScript client setup](./getting-started.md) diff --git a/Documentation/reactors.md b/Documentation/reactors.md index 9e9fe7e..81b72d3 100644 --- a/Documentation/reactors.md +++ b/Documentation/reactors.md @@ -1,47 +1,13 @@ -# Reactors - -Reactors are the "if this then that" of event sourcing — they observe events and produce side effects. Unlike projections (which build state), reactors *do things*: send notifications, trigger commands in other contexts, call external APIs. - -## Defining a Reactor - -Use the `@reactor` decorator to mark a class as a reactor: - -```typescript -import { reactor, EventContext, eventType } from '@cratis/chronicle'; - -@eventType() -class EmployeeHired { - constructor(readonly firstName: string, readonly lastName: string) {} -} +--- +sharedTopicBridge: true +--- -@reactor() -class HrNotificationReactor { - async employeeHired(event: EmployeeHired, context: EventContext): Promise { - console.log(`${event.firstName} ${event.lastName} was hired! (seq: ${context.sequenceNumber})`); - } -} -``` - -## Rules for Reactors - -1. **Idempotent** — Reactors may be called more than once for the same event (e.g. during replay). Design accordingly. -2. **Use event data directly** — Never query the read model back inside a reactor. The event contains all the information you need. -3. **Single responsibility** — Each reactor class should have a focused purpose. -4. **No state** — Reactors should be stateless. Inject dependencies via the constructor, but do not store mutable state. - -## Method Dispatch - -Method dispatch is by convention: the first parameter type of each public method determines which events it handles. +# Reactors -```typescript -@reactor() -class MyReactor { - // Handles EmployeeHired events - async employeeHired(event: EmployeeHired, context: EventContext): Promise { ... } +Reactors are documented in the shared Chronicle docs with synchronized examples for C#, Kotlin, Java, Elixir, and TypeScript. - // Handles EmployeePromoted events - async employeePromoted(event: EmployeePromoted, context: EventContext): Promise { ... } -} -``` +- [Getting started with reactors](/chronicle/reactors/getting-started/) +- [Reactor event processing](/chronicle/reactors/event-processing/) +- [Reactors overview](/chronicle/reactors/) -The `context` parameter is optional — omit it if you don't need event metadata. +Use the [TypeScript get started page](/chronicle/clients/typescript/getting-started/) for package installation and connection setup. diff --git a/Documentation/read-models.md b/Documentation/read-models.md index 4d9b16b..906a286 100644 --- a/Documentation/read-models.md +++ b/Documentation/read-models.md @@ -1,37 +1,13 @@ -# Read Models - -Use `eventStore.readModels` to query, watch, and dehydrate read model instances that are produced by projections or reducers. - -## Get a Single Read Model - -```typescript -import { ChronicleClient, ChronicleOptions } from '@cratis/chronicle'; -import { EmployeeState } from '../Samples/Console/reducers'; - -const client = new ChronicleClient(ChronicleOptions.development()); -const store = await client.getEventStore('TestStore'); +--- +sharedTopicBridge: true +--- -const employee = await store.readModels.getInstanceById(EmployeeState, 'a0000001-0000-0000-0000-000000000000'); -console.log(employee.title); -``` - -## Get All Read Model Instances - -```typescript -const employees = await store.readModels.getInstances(EmployeeState); -console.log(employees.length); -``` - -## Watch for Changes - -```typescript -for await (const changeset of store.readModels.watch(EmployeeState)) { - console.log(changeset.key, changeset.readModel.title, changeset.removed); -} -``` +# Read Models -## Notes +Read models are shared Chronicle concepts. Querying, snapshots, watching, and consistency are documented in the shared Chronicle section. -- The read model type must be discoverable by Chronicle, just like projections and reducers. -- Reducer-backed read models should be declared on the `@reducer(..., readModelType)` decorator when the reducer class name differs from the read model type. -- Read models are registered automatically when an event store connects and registers artifacts. +- [Read models](/chronicle/read-models/) +- [Getting a single read model](/chronicle/read-models/getting-single-instance/) +- [Getting read model collections](/chronicle/read-models/getting-collection-instances/) +- [Watching read models](/chronicle/read-models/watching-read-models/) +- [TypeScript client setup](./getting-started.md) diff --git a/Documentation/reducers.md b/Documentation/reducers.md index 4af480e..566f301 100644 --- a/Documentation/reducers.md +++ b/Documentation/reducers.md @@ -1,52 +1,12 @@ -# Reducers - -Reducers observe events from an event sequence and fold them into a read model (state). Unlike reactors, they produce state — they are the "how did we get here" of event sourcing. - -## Defining a Reducer - -Use the `@reducer` decorator to mark a class as a reducer: - -```typescript -import { reducer, EventContext, eventType } from '@cratis/chronicle'; - -interface EmployeeState { - firstName: string; - lastName: string; - title: string; -} +--- +sharedTopicBridge: true +--- -@eventType() -class EmployeeHired { - constructor( - readonly firstName: string, - readonly lastName: string, - readonly title: string - ) {} -} - -@eventType() -class EmployeePromoted { - constructor(readonly newTitle: string) {} -} - -@reducer() -class EmployeeStateReducer { - async employeeHired(event: EmployeeHired, state?: EmployeeState): Promise { - return { firstName: event.firstName, lastName: event.lastName, title: event.title }; - } - - async employeePromoted(event: EmployeePromoted, state?: EmployeeState): Promise { - return { ...state!, title: event.newTitle }; - } -} -``` - -## Rules for Reducers - -1. **Pure functions** — Given the same event and state, always return the same new state. -2. **Handle undefined state** — The initial state may be `undefined` (no events yet). Handle this case gracefully. -3. **Single read model** — A reducer produces a single read model type. Build multiple reducers for different views. +# Reducers -## Method Dispatch +Reducers are shared Chronicle read-model behavior. Use the shared reducer docs for the model, lifecycle, filtering, and client-tabbed examples. -Method dispatch is by convention: the first parameter type of each public method determines which events it handles. The second parameter is the current state (may be `undefined` for the first event). +- [Reducers](/chronicle/reducers/) +- [Getting started with reducers](/chronicle/reducers/getting-started/) +- [Reducer event processing](/chronicle/reducers/event-processing/) +- [TypeScript client setup](./getting-started.md) diff --git a/Documentation/seeding.md b/Documentation/seeding.md index 200cfa6..12359f7 100644 --- a/Documentation/seeding.md +++ b/Documentation/seeding.md @@ -1,48 +1,107 @@ # Seeding -Seeding lets you populate event streams with initial events when your client starts. +This page shows how to seed events using the Chronicle TypeScript client. Seeding is sent to the Chronicle Server when the event store connects, and the server applies it once per namespace. See [Event Seeding](/chronicle/event-seeding/) for the concept this page assumes. -## Defining a Seeder - -Use `@seeder` on a class that implements `ICanSeedEvents`: +## Define events ```typescript -import { ICanSeedEvents, IEventSeedingBuilder, eventType, seeder } from '@cratis/chronicle'; +import { eventType } from '@cratis/chronicle'; @eventType() -class ProjectRegistered { - constructor(readonly name: string) {} +class AccountOpened { + constructor( + readonly accountId: string, + readonly ownerName: string, + readonly initialBalance: number + ) {} } @eventType() -class ProjectRenamed { - constructor(readonly name: string) {} +class FundsDeposited { + constructor( + readonly accountId: string, + readonly amount: number + ) {} } +``` + +## Implement a seeder + +Decorate a class with `@seeder()`, implement `ICanSeedEvents`, and use the builder's `for`/`forEventSource` to accumulate events: + +```typescript +import { seeder, ICanSeedEvents, IEventSeedingBuilder } from '@cratis/chronicle'; @seeder() -class InitialProjectSeeder implements ICanSeedEvents { - seed(builder: IEventSeedingBuilder) { - builder - .for('project-1', [ - new ProjectRegistered('Accounting') - ]) - .forNamespace('Sales') - .forEventSource('project-2', [ - new ProjectRegistered('Sales'), - new ProjectRenamed('Sales EMEA') - ]); +class AccountSeeder implements ICanSeedEvents { + seed(builder: IEventSeedingBuilder): void { + builder.for('account-1', [ + new AccountOpened('account-1', 'Alice', 1000) + ]); } } ``` -## Builder API +## Seed multiple events of the same type + +```typescript +seed(builder: IEventSeedingBuilder): void { + builder.for('account-1', [ + new AccountOpened('account-1', 'Alice', 1000) + ]); +} +``` + +## Seed mixed event types + +Use `forEventSource` to seed several different event types for the same event source: + +```typescript +seed(builder: IEventSeedingBuilder): void { + builder.forEventSource('account-1', [ + new AccountOpened('account-1', 'Alice', 1000), + new FundsDeposited('account-1', 500) + ]); +} +``` + +## Namespace-scoped seed data + +By default, seed data applies to all namespaces in the event store. To target a specific namespace, use `forNamespace` to get a scoped builder: + +```typescript +seed(builder: IEventSeedingBuilder): void { + builder + .forNamespace('production') + .for('account-1', [new AccountOpened('account-1', 'Alice', 1000)]); +} +``` + +The scoped builder supports the same `for`/`forEventSource` methods as the top-level builder. Each namespace receives only its own scoped events in addition to any global events. + +## Running seeders + +Discover and register seeders explicitly through the event store: + +```typescript +import { IEventStore } from '@cratis/chronicle'; + +async function runSeeders(store: IEventStore): Promise { + await store.seeding.discover(); + await store.seeding.register(); +} +``` -- `for(eventSourceId, events)` — strongly typed seeding when all events in the collection are the same event class. -- `forEventSource(eventSourceId, events)` — seeding for mixed event classes in one collection. -- `forNamespace(namespace)` — switch to namespace-scoped seeding for subsequent calls. +## How it runs -By default, seeded events are global (applies to all namespaces). Use `forNamespace(...)` for namespace-specific seed data. +- Seed batches are sent to the Chronicle Server when the event store connects. +- The server deduplicates seeded events and applies them once per namespace. +- Events are appended in a single batch for efficient startup. -## Discovery and Registration +## Best practices -Seeders are discovered as client artifacts (just like reactors and reducers). During event store artifact registration, seed data is sent after observers are registered. +- Keep seed data minimal and deterministic. +- Use clear event source IDs to make debugging easier. +- Group seeders by scenario so you can remove or adjust them easily. +- Only call `store.seeding.discover()`/`.register()` when you want seeding to run — for example, guard it behind a development-only build flag or configuration check. +- Use `forNamespace` when seed data is tenant-specific or environment-specific to avoid polluting other namespaces. diff --git a/Documentation/sinks.md b/Documentation/sinks.md index 9fa5391..4a2040d 100644 --- a/Documentation/sinks.md +++ b/Documentation/sinks.md @@ -1,8 +1,6 @@ # Sinks -A **sink** is where Chronicle persists the read models produced by your projections and reducers. When you register a projection, reducer, or model-bound read model, Chronicle needs to know which storage technology should hold the resulting state. That choice is expressed as a **sink type**. - -By default the client uses **MongoDB**, so most applications never need to think about sinks at all. You only configure a sink type when you want your read models stored somewhere else — for example, a SQL database. +See [Sinks](/chronicle/sinks/) for what a sink is and the storage backends Chronicle ships with. By default the client uses **MongoDB**, so most applications never need to think about sinks at all. You only configure a sink type when you want your read models stored somewhere else — for example, a SQL database. ## Well-Known Sink Types diff --git a/Documentation/toc.yml b/Documentation/toc.yml new file mode 100644 index 0000000..2ddc7c2 --- /dev/null +++ b/Documentation/toc.yml @@ -0,0 +1,20 @@ +- name: TypeScript Client + href: index.md +- name: Get Started + href: getting-started.md +- name: Runtime + items: + - name: Sinks + href: sinks.md + - name: Jobs + href: jobs.md + - name: Webhooks + href: webhooks.md + - name: Seeding + href: seeding.md + - name: Identity + href: identity.md + - name: Causation + href: auditing.md + - name: Correlation + href: correlation.md diff --git a/Documentation/transactions.md b/Documentation/transactions.md index d94621a..cf043a4 100644 --- a/Documentation/transactions.md +++ b/Documentation/transactions.md @@ -1,50 +1,11 @@ -# Transactions (Unit of Work) +--- +sharedTopicBridge: true +--- -The Chronicle TypeScript client supports transaction-scoped event appends through a Unit of Work API. +# Transactions and Unit of Work -## Start a Unit of Work +Unit-of-work transactions are now documented as a shared Chronicle workflow with synchronized client examples. -```typescript -const store = await client.getEventStore('MyStore'); -const unitOfWork = store.unitOfWorkManager.begin(); -``` - -## Append Through the Transactional Event Sequence - -Use `eventLog.transactional` to stage events in the current unit of work: - -```typescript -await store.eventLog.transactional.append('order-123', new OrderCreated()); - -await store.eventLog.transactional.appendMany('order-123', [ - new OrderLineAdded('sku-1', 2), - new OrderConfirmed() -]); -``` - -These calls buffer events in the unit of work. They do not append immediately. - -## Commit or Roll Back - -```typescript -await unitOfWork.commit(); -``` - -If you need to discard staged events: - -```typescript -await unitOfWork.rollback(); -``` - -## Appending to Multiple Event Sources in One Unit of Work - -You can stage events across event sources, then commit once: - -```typescript -await store.eventLog.transactional.append('customer-42', new CustomerUpdated()); -await store.eventLog.transactional.append('order-123', new OrderConfirmed()); - -await unitOfWork.commit(); -``` - -Internally, commit uses `appendMany(eventsForEventSourceId, options?)` support on event sequences. +- [Transactions and unit of work](/chronicle/events/transactions/) +- [Appending many events](/chronicle/events/appending-many/) +- [TypeScript client setup](./getting-started.md) diff --git a/Documentation/validate-client-snippets.py b/Documentation/validate-client-snippets.py new file mode 100644 index 0000000..ad05c6e --- /dev/null +++ b/Documentation/validate-client-snippets.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# Copyright (c) Cratis. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import json +import re +import shutil +import subprocess +import sys +import textwrap +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SOURCE_ROOT = REPO_ROOT / "Source" +SNIPPET_ROOT = REPO_ROOT / "Documentation" / "client-snippets" +GENERATED_DIR = SOURCE_ROOT / ".docs-snippets" +GENERATED_SOURCE = GENERATED_DIR / "snippets.ts" +GENERATED_TSCONFIG = GENERATED_DIR / "tsconfig.json" +FENCE_RE = re.compile(r"```([^\s`]+)[^\n]*\n(.*?)\n```", re.DOTALL) +NAMED_IMPORT_RE = re.compile(r"^import\s+\{([^}]+)\}\s+from\s+['\"]@cratis/chronicle['\"];?\s*$") +CONTRACTS_NAMED_IMPORT_RE = re.compile(r"^import\s+\{([^}]+)\}\s+from\s+['\"]@cratis/chronicle\.contracts['\"];?\s*$") +FUNDAMENTALS_NAMED_IMPORT_RE = re.compile(r"^import\s+\{([^}]+)\}\s+from\s+['\"]@cratis/fundamentals['\"];?\s*$") +SIDE_EFFECT_IMPORT_RE = re.compile(r"^import\s+['\"]([^'\"]+)['\"];?\s*$") +UNSUPPORTED_SNIPPET_MARKER = "does not support this workflow yet" +VALIDATION_EXCLUDED_PREFIXES = ("legacy/",) + +BODY_SNIPPETS = { + "get-started/client-flow": "", + "events/appending/schema-validation": """ + const eventSourceId = 'order-123'; + const customerId = 'customer-42'; + const total = 42; + """, + "read-models/getting-single-instance/basic": """ + const accountId = 'account-42'; + """, + "read-models/getting-collection-instances/basic": "", + "read-models/getting-collection-instances/filtering": """ + const threshold = 1000; + """, + "read-models/getting-collection-instances/event-count": "", + "read-models/getting-snapshots/basic": """ + const orderId = 'order-123'; + """, + "read-models/getting-snapshots/analyze": """ + const orderId = 'order-123'; + """, + "read-models/watching-read-models/basic": "", + "read-models/watching-read-models/filtering": """ + const threshold = 1000; + """, + "contributing/clients/typescript-grpc-package/event-stores-definition": "", + "contributing/clients/typescript-grpc-package/namespaces-definition": "", + "contributing/clients/typescript-grpc-package/request-messages": "", + "contributing/clients/typescript-grpc-package/service-types": "", +} + +COMMON_DECLARATIONS = [ + """ + class AccountInfo { + name = ''; + balance = 0; + } + """, + """ + class Account { + id = ''; + name = ''; + balance = 0; + createdDate = new Date(); + } + """, + """ + enum OrderStatus { + New = 'New', + Confirmed = 'Confirmed', + Shipped = 'Shipped', + Completed = 'Completed' + } + """, + """ + class Order { + id = ''; + status = OrderStatus.New; + totalAmount = 0; + } + """, +] + + +def snippet_files() -> list[Path]: + files = [ + path + for path in sorted([*SNIPPET_ROOT.rglob("*.md"), *SNIPPET_ROOT.rglob("*.mdx")]) + if not snippet_key(path).startswith(VALIDATION_EXCLUDED_PREFIXES) + ] + snippets = {} + for path in files: + key = snippet_key(path) + if key in snippets: + raise ValueError(f"Duplicate client snippet {key}: {snippets[key]} and {path.relative_to(REPO_ROOT)}") + snippets[key] = path.relative_to(REPO_ROOT) + return files + + +def snippet_key(path: Path) -> str: + return path.relative_to(SNIPPET_ROOT).with_suffix("").as_posix() + + +def extract_snippet(path: Path) -> str | None: + raw = path.read_text(encoding="utf-8") + matches = FENCE_RE.findall(raw) + if len(matches) != 1: + raise ValueError(f"{path.relative_to(REPO_ROOT)} must contain exactly one fenced TypeScript snippet") + + language, code = matches[0] + if language == "text" and UNSUPPORTED_SNIPPET_MARKER in code: + return None + if language != "typescript": + raise ValueError(f"{path.relative_to(REPO_ROOT)} must use a typescript code fence, got {language!r}") + + return code.strip() + + +def split_imports( + code: str, + named_imports: set[str], + contracts_named_imports: set[str], + fundamentals_named_imports: set[str], + side_effect_imports: set[str], +) -> str: + body: list[str] = [] + for line in code.splitlines(): + named_match = NAMED_IMPORT_RE.match(line) + if named_match: + for imported in named_match.group(1).split(","): + imported = imported.strip() + if imported: + named_imports.add(imported) + continue + + contracts_named_match = CONTRACTS_NAMED_IMPORT_RE.match(line) + if contracts_named_match: + for imported in contracts_named_match.group(1).split(","): + imported = imported.strip() + if imported: + contracts_named_imports.add(imported) + continue + + fundamentals_named_match = FUNDAMENTALS_NAMED_IMPORT_RE.match(line) + if fundamentals_named_match: + for imported in fundamentals_named_match.group(1).split(","): + imported = imported.strip() + if imported: + fundamentals_named_imports.add(imported) + continue + + side_effect_match = SIDE_EFFECT_IMPORT_RE.match(line) + if side_effect_match: + side_effect_imports.add(side_effect_match.group(1)) + continue + + body.append(line) + + return "\n".join(body).strip() + + +def function_name(relative_path: str) -> str: + return "snippet_" + re.sub(r"[^A-Za-z0-9_]", "_", relative_path) + + +def generate_source() -> str: + files = snippet_files() + if not files: + raise ValueError(f"No client snippets found in {SNIPPET_ROOT}") + + named_imports = {"IEventStore"} + contracts_named_imports: set[str] = set() + fundamentals_named_imports: set[str] = set() + side_effect_imports = {"reflect-metadata"} + declarations: list[str] = [textwrap.dedent(declaration).strip() for declaration in COMMON_DECLARATIONS] + functions: list[str] = [] + + for path in files: + relative_path = snippet_key(path) + snippet = extract_snippet(path) + if snippet is None: + continue + + body = split_imports(snippet, named_imports, contracts_named_imports, fundamentals_named_imports, side_effect_imports) + + if relative_path in BODY_SNIPPETS: + prelude = textwrap.dedent(BODY_SNIPPETS[relative_path]).strip() + lines = [line for line in [prelude, body] if line] + function_body = textwrap.indent("\n\n".join(lines), " ") + functions.append(f"async function {function_name(relative_path)}(store: IEventStore): Promise {{\n{function_body}\n}}") + else: + declarations.append(body) + + imports = [ + *[f"import '{module_name}';" for module_name in sorted(side_effect_imports)], + f"import {{ {', '.join(sorted(named_imports))} }} from '../index';", + ] + if contracts_named_imports: + imports.append(f"import {{ {', '.join(sorted(contracts_named_imports))} }} from '@cratis/chronicle.contracts';") + if fundamentals_named_imports: + imports.append(f"import {{ {', '.join(sorted(fundamentals_named_imports))} }} from '@cratis/fundamentals';") + + return "\n\n".join([ + "// This file is generated by Documentation/validate-client-snippets.py.", + *imports, + *declarations, + *functions, + "", + ]) + + +def generate_tsconfig() -> str: + config = { + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": True, + "noUnusedLocals": False, + "noUnusedParameters": False, + }, + "include": ["snippets.ts"], + } + return json.dumps(config, indent=4) + "\n" + + +def main() -> int: + GENERATED_DIR.mkdir(parents=True, exist_ok=True) + GENERATED_SOURCE.write_text(generate_source(), encoding="utf-8") + GENERATED_TSCONFIG.write_text(generate_tsconfig(), encoding="utf-8") + + try: + subprocess.run(["yarn", "exec", "tsc", "-p", ".docs-snippets/tsconfig.json"], cwd=SOURCE_ROOT, check=True) + finally: + shutil.rmtree(GENERATED_DIR, ignore_errors=True) + + print("TypeScript Chronicle client snippets compiled successfully.") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"Client snippet validation failed: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/Documentation/webhooks.md b/Documentation/webhooks.md index 492f85a..a692b24 100644 --- a/Documentation/webhooks.md +++ b/Documentation/webhooks.md @@ -1,6 +1,6 @@ # Webhooks -Use `eventStore.webhooks` to register webhook observers for event streams. +See [Webhooks](/chronicle/webhooks/) for what a webhook is. Use `eventStore.webhooks` to register webhook observers for event streams. ## Register a webhook programmatically