{"_id":"@a-it/event-sourcing","name":"@a-it/event-sourcing","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@a-it/event-sourcing","version":"1.0.0","description":"NestJS Event Sourcing Library","author":{"name":"George Pogosyan"},"license":"MIT","private":false,"publishConfig":{"access":"public"},"main":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc -p tsconfig.build.json","ci":"biome ci ./lib","format":"biome format ./lib ./tests ./example --write","lint":"biome check ./lib ./tests ./example --apply","test":"jest --config jest.config.js --runInBand","test:ci":"jest --config jest.config.js --runInBand --coverage","run:example":"node -r @swc-node/register ./example/src/main.ts","prepublish:npm":"npm run build","publish:npm":"npm run publish --access public","prepublish:next":"npm run build","publish:next":"npm run publish --access public --tag next","prerelease":"npm run build","semantic-release":"semantic-release","docs:dev":"vitepress dev docs","docs:build":"vitepress build docs","docs:preview":"vitepress preview docs","docs:publish":"mv docs/.vitepress/dist ./public"},"engines":{"node":">=20.0.0"},"dependencies":{"class-transformer":"0.5.1","ulid":"^2.3.0"},"peerDependencies":{"@aws-sdk/client-dynamodb":"^3.180.0","@aws-sdk/util-dynamodb":"^3.180.0","@nestjs/common":"^10.0.0","@nestjs/core":"^10.0.0","mongodb":"^6.0.0","reflect-metadata":"^0.2.0","rxjs":"^7.2.0"},"peerDependenciesMeta":{"@aws-sdk/client-dynamodb":{"optional":true},"@aws-sdk/util-dynamodb":{"optional":true},"mongodb":{"optional":true}},"devDependencies":{"@aws-sdk/client-dynamodb":"3.529.1","@aws-sdk/util-dynamodb":"3.529.1","@biomejs/biome":"1.5.3","@faker-js/faker":"^8.0.0","@nestjs/common":"10.3.3","@nestjs/core":"10.3.3","@nestjs/platform-express":"10.3.3","@nestjs/testing":"10.3.3","@semantic-release/git":"^10.0.1","@semantic-release/gitlab":"^13.0.0","@semantic-release/npm":"^12.0.0","@swc-node/register":"^1.5.2","@swc/core":"1.3.105","@swc/jest":"0.2.36","@types/jest":"29.5.12","@types/node":"20.11.28","jest":"29.7.0","jest-mock":"29.7.0","mongodb":"6.3.0","mongodb-memory-server":"9.1.6","reflect-metadata":"0.2.1","release-it":"17.1.1","rxjs":"7.8.1","semantic-release":"^23.0.0","typescript":"5.4.2","vitepress":"^1.6.3"},"repository":{"type":"git","url":"git@gitlab.a-it.org:ait/event-sourcing.git"},"_id":"@a-it/event-sourcing@1.0.0","gitHead":"7936ffa8504e07998adddf6c63321c66a77c6338","_nodeVersion":"20.10.0","_npmVersion":"10.5.0","dist":{"integrity":"sha512-gHkcc+8/wHiW967cO8A6BBOhLITLJCOrVdvadOXlaPKUHm84urok6Nj4BHIPrmj5LkLoHeUQRL+gwGPi0XChWQ==","shasum":"231e90aeeb7dea14e1b79be67abe64b74791b4d3","tarball":"https://registry.npmjs.org/@a-it/event-sourcing/-/event-sourcing-1.0.0.tgz","fileCount":251,"unpackedSize":223005,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIH56tM7a0r+gnUJ4oZ3V8GOcW9JgQjd2h41LE3DbbiOtAiBuwJxazCHWUxDiOiLi/X+rhHG1rC2UBLa5Wqh+3Zpdkw=="}]},"_npmUser":{"name":"g.pogos","email":"g.pogos@ya.ru"},"directories":{},"maintainers":[{"name":"g.pogos","email":"g.pogos@ya.ru"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/event-sourcing_1.0.0_1741090335501_0.288981211828661"},"_hasShrinkwrap":false}},"time":{"created":"2025-03-04T12:12:15.434Z","1.0.0":"2025-03-04T12:12:15.676Z","modified":"2025-03-04T12:12:15.927Z"},"maintainers":[{"name":"g.pogos","email":"g.pogos@ya.ru"}],"description":"NestJS Event Sourcing Library","repository":{"type":"git","url":"git@gitlab.a-it.org:ait/event-sourcing.git"},"author":{"name":"George Pogosyan"},"license":"MIT","readme":"&nbsp;\n<details open>\n  <summary>Table of Contents</summary>\n  <ol>\n    <li><a href=\"#getting-started\">Getting started</a></li>\n    <li><a href=\"#aggregates--value-objects\">Aggregates & value objects</a></li>\n    <li><a href=\"#commands--command-handlers\">Commands & command handlers</a></li>\n    <li>\n    <a href=\"#events\">Events</a>\n    <ul>\n          <li><a href=\"#event-streams\">Event streams</a></li>\n          <li><a href=\"#event-store\">Event store</a></li>\n      <li><a href=\"#event-listeners\">Event listeners</a></li>\n      </ul>\n  </li>\n  <li>\n    <a href=\"#snapshots\">Snapshots</a>\n    <ul>\n          <li><a href=\"#snapshot-streams\">Snapshot streams</a></li>\n          <li><a href=\"#snapshot-store\">Snapshot store</a></li>\n      </ul>\n  </li>\n  <li><a href=\"#aggregate-repositories\">Aggregate repositories</a></li>\n    <li><a href=\"#queries\">Queries</a></li>\n    <li><a href=\"#misc\">Misc</a></li>\n    <li><a href=\"#contact\">Contact</a></li>\n    <li><a href=\"#acknowledgments\">Acknowledgments</a></li>\n  </ol>\n</details>\n&nbsp;\n<hr/>\n&nbsp;\n\n## Getting started\nДля того, чтоб установить данную библиотеку, выполните следующую команду:\n```\nnpm install @a-it/event-sourcing\n```\nСейчас библиотека из коробки имеет враппер для сохранения событий и снепшотов в MongoDB. Для того, чтоб использовать эту возможность, необходимо установить соответсвующую зависимость:\n```\nnpm install mongodb # Для использования MongoDB\n```\nДля тестирования и ознакомления нет необходимости использовать никакую БД, т.к. из коробки поддерживается in-memory хранилище.\n\nКак только все зависимости устоновленны, мы можете импортировать EventSourcingModule в коренной AppModule вашего приложения. Конфигуряция будет зависить от того, какой тип хранилища вы хотите использовать и планируете ли вы использовать снепшоты.\n```typescript\nimport { EventSourcingModule } from '@a-it/event-sourcing';\nimport { Events } from './app.providers.ts';\n\n@Module({\n  imports: [\n    EventSourcingModule.forRoot({\n      eventStore: {\n        client: 'mongodb',\n        options: { \n          url: 'mongodb://127.0.0.1:27017' \n        },\n      },\n      snapshotStore: {\n        client: 'mongodb',\n        options: { \n          url: 'mongodb://127.0.0.1:27017' \n        },\n      },\n      events: [...Events],\n    }),\n})\nexport class AppModule {}\n```\n&nbsp;\n\n## Aggregates & value objects\nАгрегат моделирует отдельную концепцию, которая имеет уникальную идентичность в вашем приложении, например Account.\n\nЧтобы создать агрегат с использованием этой библиотеки, вам необходимо:\n- наследовать класс AggregateRoot, который отвечает за обработку событий и отслеживание версии агрегата\n- применить декоратор `@Aggregate()`\n\n```typescript\nimport { Aggregate, AggregateRoot } from '@a-it/event-sourcing';\n\n@Aggregate('account')\nclass Account extends AggregateRoot {\n  ...\n}\n```\n\nДекоратор `@Aggregate()` помечает класс как агрегат и при необходимости указывает, как следует называть идентификатор потока событий и снимков, например: `@Aggregate({streamName: 'account' })` создаст следующий идентификатор потока: `account-01ARZ3NDEKTSV4RRFFQ69G5FAV`. Если имя потока не указано в декораторе, имя класса будет автоматически преобразовано в нижний регистр и использовано.\n\nОбъект значения — это неизменяемая модель, не имеющая концептуальной идентичности, она описывает характеристики и, при необходимости, требует некоторой проверки, например: имя учетной записи. Чтобы создать объект значения, мы можем просто расширить класс ValueObject.\n\n```typescript\nimport { ValueObject } from '@a-it/event-sourcing';\n\nexport class AccountName extends ValueObject {\n  public static fromString(name: string) {\n    if(name.length < 3) {\n      throw new Error('Account name should contain at least 3 characters');\n    }\n    return new Accountname({ value: name });\n  }\n\n  get value(): string {\n    return this.props.value;\n  }\n}\n```\n&nbsp;\n\n## Commands & command handlers\nКоманда — это объект, отправляемый в приложение вашего домена, который описывает намерение пользователя и обрабатывается CommandHandler. В идеале имя команды подразумевает агрегат, с которым она работает, и ее назначение в обязательном порядке, например OpenAccountCommand.\n\n```typescript\nimport { ICommand } from '@a-it/event-sourcing';\n\nclass OpenAccountCommand implements ICommand {\n  constructor(public readonly accountOwner: string) {}\n}\n```\n\nЗатем вы можете определить CommandHandler, который будет отвечать за обработку каждого выполнения этой команды.\n\n```typescript\nimport { CommandHandler, ICommandHandler } from '@a-it/event-sourcing';\n\n@CommandHandler(OpenAccountCommand)\nexport class OpenAccountCommandHandler implements ICommandHandler {\n\n  constructor(private readonly accountRepository: AccountRepository) {}\n\n  async execute(command: OpenAccountCommand): Promise<string> {\n    const accountId = AccountId.generate();\n    const account = Account.open(accountId, command.accountOwnerIds?.map(AccountOwnerId.from));\n\n    await this.accountRepository.save(account);\n\n    return accountId.value;\n  }\n}\n```\n\nНе забудьте зарегистрировать CommandHandlers в качестве провайдеров в своем приложении.\n&nbsp;\n\n## Events\nСобытия — это классы, описывающие произошедший факт. Их можно создать с помощью декоратора @Event() и необходимо зарегистрировать в EventSourcingModule. Если имя явно не указано, используется имя самого класса, в противном случае указанное имя добавляется в качестве метаданных в ваш класс. Имя вашего события используется внутри приложения для создания карты событий в вашем приложении и при необходимости связывает это событие с кастомным сериализатором событий.\n\n```typescript\n@Event('account-opened')\nexport class AccountOpenedEvent implements IEvent {\n  constructor(\n    public readonly accountId: string,\n    public readonly openedOn: string,\n    public readonly accountOwnerIds?: string[]\n  ) {}\n}\n```\n\nПредпочтительно, чтобы события содержали только примитивные значения, иначе это может вызвать проблемы при их сохранении и чтении из базы данных. Однако, чтобы опосредовать это, всякий раз, когда событие необходимо сохранить или извлечь из базы данных, оно (де) сериализуется с использованием библиотеки [class-transformer](https://github.com/typestack/class-transformer), однако вы можете написать свою собственную логику сериализатора для события. Если вы решите это сделать, не забудьте зарегистрировать сериализаторы событий в качестве поставщиков в своем приложении.\n\n```typescript\n@Event('account-opened')\nexport class AccountOpenedEvent implements IEvent {\n  constructor(\n    public readonly accountId: AccountId,\n    public readonly openedOn: Date,\n    public readonly accountOwnerIds?: AccountOwnerId[]\n  ) {}\n}\n\n@EventSerializer(AccountOpenedEvent)\nexport class AccountOpenedEventSerializer implements IEventSerializer {\n  serialize({ accountId, openedOn, accountOwnerIds }: AccountOpenedEvent): IEventPayload<AccountOpenedEvent> {\n    return {\n      accountId: accountId.value,\n      openedOn: openedOn.toISOString(),\n      accountOwnerIds: accountOwnerIds?.map((id) => id.value)\n    };\n  }\n\n  deserialize({ id, openedOn, accountOwnerIds }: IEventPayload<AccountOpenedEvent>): AccountOpenedEvent {\n    const accountId = AccountId.from(id);\n    const openedOnDate = openedOn && new Date(openedOn);\n    const ownerIds = accountOwnerIds?.map((id) => AccountOwnerId.from(id));\n\n    return new AccountOpenedEvent(accountId, openedOnDate, ownerIds);\n  }\n}\n```\n&nbsp;\n\n### Event streams\nКласс EventStream создает представление потока событий для определенного агрегата.\n\n```typescript\nconst accountId = Id.generate();\nconst stream = EventStream.for(Account, accountId); \n\nstream.streamId; // account-01ARZ3NDEKTSV4RRFFQ69G5FAV\n```\n&nbsp;\n\n### Event store\nЭта библиотека предоставляет несколько типов реализаций хранилища событий, как описано выше.\nВажно запустить метод установки в хранилище, чтобы подготовить базу данных для хранения ваших событий. По сути, это создает таблицу или коллекцию «событий» или «снимков».\n\n```typescript\nimport { EventStore } from '@a-it/event-sourcing';\n\nclass AppModule implements OnModuleInit {\n\n  constructor(private readonly eventStore: EventStore) {}\n\n  async onModuleInit() {\n    await this.eventStore.setup();\n  }\n}\n```\n&nbsp;\n\n### Event envelopes\nСобытия, сохраняемые в потоке, всегда упаковываются в EventEnvelope. Оно содержит имя события, указанное с помощью декоратора @Event(), сериализованную версию события и дополнительные метаданные. (идентификатор события, идентификатор агрегата, версия и т. д.)\n\n### Event publishers\nВсякий раз, когда EventStore добавляет события, созданные EventEnvelopes публикуются EventPublishers, зарегистрированными в EventBus. EventPublisher по умолчанию занимается внутренней публикацией событий, что позволяет нам создавать и регистрировать EventHandlers, которые автоматически реагируют на эти события.\n\n```typescript\n@EventHandler(AccountOpenedEvent)\nexport class AccountOpenedEventHandler implements IEventHandler {\n\thandle(envelope: EventEnvelope<AccountOpenedEvent>) {\n\t\t...\n\t}\n}\n```\n\nЧтобы зарегистрировать дополнительный EventPublisher для передачи ваших EventEnvelopes в Redis, SNS, Kafka и т. д., просто создайте его и зарегистрируйте в качестве поставщика.\n\n```typescript\n@EventPublisher()\nexport class CustomEventPublisher implements IEventPublisher {\n\tasync publish(envelope: EventEnvelope<IEvent>): Promise<void> {\n\t\t...\n\t}\n}\n```\n\n## Snapshots\nСнимки — это оптимизация, которая совершенно необязательна. Однако они пригодятся, когда потоки событий становятся большими и их чтение становится медленным.\n&nbsp;\n\n### Snapshot streams\nКласс SnapshotStream создает представление потока снимков для определенного агрегата.\n\n```typescript\nconst accountId = Id.generate();\nconst stream = SnapshotStream.for(Account, accountId);\n\nstream.streamId // account-01ARZ3NDEKTSV4RRFFQ69G5FAV\n```\n&nbsp;\n\n### Snapshot store\nSnapshotStore сохраняет состояние агрегата через определенный интервал и извлекает события только из этой версии. Как и EventStore, его необходимо настроить, при необходимости с помощью пула клиентов.\nЕще одним преимуществом использования обработчика моментальных снимков является то, что он также создает снимок версии 1 вашего агрегата, что упрощает получение полного набора агрегатов определенного типа в вашем приложении.\n\n```typescript\nimport { SnapshotStore } from '@a-it/event-sourcing';\n\nclass AppModule implements OnModuleInit {\n\n  constructor(private readonly snapshotStore: SnapshotStore) {}\n\n  async onModuleInit() {\n    await this.snapshotStore.setup();\n  }\n}\n```\n&nbsp;\n\n### Snapshot handlers\nХранилище используется за кулисами базового класса SnapshotHandler, который отвечает за скрытое сохранение и загрузку снимков.\n\nЗа то, как агрегатный снимок (де)сериализуется, отвечает SnapshotHandler, который расширяет базу и декорируется декоратором `@Snapshot()`, который определяет:\n- за какой агрегат он отвечает\n- имя потока (по умолчанию имя класса агрегата)\n- через какой интервал должен быть сделан снимок\n\n```typescript\nimport { SnapshotHandler } from '@a-it/event-sourcing';\n\n@Snapshot(Account, { name: 'account', interval: 5 })\nexport class AccountSnapshotHandler extends SnapshotHandler<Account> {\n  serialize({ id, ownerIds, balance, openedOn, closedOn }: Account) {\n    return {\n      id: id.value,\n      ownerIds: ownerIds.map(({ value }) => value),\n      balance,\n      openedOn: openedOn ? openedOn.toISOString() : undefined,\n      closedOn: closedOn ? closedOn.toISOString() : undefined,\n    };\n  }\n  deserialize({ id, ownerIds, balance, openedOn, closedOn }: ISnapshot<Account>): Account {\n    const account = new Account();\n    account.id = AccountId.from(id);\n    account.ownerIds = ownerIds.map(AccountOwnerId.from);\n    account.balance = balance;\n    account.openedOn = openedOn && new Date(openedOn);\n    account.closedOn = closedOn && new Date(closedOn);\n\n    return account;\n  }\n}\n```\n&nbsp;\n\n## Aggregate repositories\nРепозитории агрегатов — это место, где встречаются оба хранилища. Например:\n```typescript\n@Injectable()\nexport class AccountRepository {\n\n  constructor(\n    private readonly eventStore: EventStore,\n    private readonly accountSnapshotHandler: AccountSnapshotHandler,\n  ) {}\n\n  async getById(accountId: AccountId) {\n    const eventStream = EventStream.for<Account>(Account, accountId);\n\n    const account = await this.accountSnapshotHandler.load(accountId);\n\n    const events = this.eventStore.getEvents(eventStream, { fromVersion: account.version + 1 });\n\n    await account.loadFromHistory(events);\n\n    if (account.version < 1) {\n        throw new AccountNotFoundException(accountId.value);\n    }\n\n    return account;\n  }\n\n  async save(account: Account): Promise<void> {\n    const events = account.commit();\n    const stream = EventStream.for<Account>(Account, account.id);\n\n    await Promise.all([\n      this.accountSnapshotHandler.save(account.id, account),\n      this.eventStore.appendEvents(stream, account.version, events),\n    ]);\n  }\n}\n```\n&nbsp;\n\n## Queries\nВы можете создавать запросы для возврата необходимых вам данных.\n```typescript\nexport class GetAccountQuery {\n\n  constructor(public readonly accountId: string) {}\n\n}\n\n@QueryHandler(GetAccountQuery)\nexport class GetAccountQueryHandler implements IQueryHandler {\n\n  constructor(private readonly accountRepository: AccountRepository) {}\n\n  async execute(query: GetAccountQuery): Promise<Account> {\n    const accountId = AccountId.from(query.accountId);\n    const account = await this.accountRepository.getById(accountId);\n\n    return account;\n  }\n}\n```\n&nbsp;\n\n## Разное\n- **А как насчет материализованных представлений?**\nВ статьях о поиске событий часто предлагается прослушивать опубликованные события, чтобы создать или обновить представление базы данных, оптимизированное для чтения. Хотя это дает некоторые преимущества, при этом необходимо учитывать много накладных расходов. Альтернатива — просто прочитать ваши модели записи. Очень интересную информацию о преимуществах и недостатках можно найти [здесь](https://www.eventstore.com/blog/live-projections-for-read-models-with-event-source-and-cqrs). .\n\n- **А как насчет саг?**\nНа данный момент я не создавал саги, потому что в базовых случаях использования EventHandlers может позаботиться о запуске побочных эффектов.\n&nbsp;\n","readmeFilename":"README.md"}