New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More β†’
Socket
Sign inDemoInstall
Socket

@castore/core

Package Overview
Dependencies
Maintainers
4
Versions
64
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@castore/core - npm Package Compare versions

Comparing version 1.3.1 to 1.4.0

dist/cjs/utils.js

3

dist/cjs/command/command.js

@@ -29,4 +29,3 @@ "use strict";

exports.tuple = tuple;
var Command = /*#__PURE__*/(0, _createClass2["default"])( // @ts-ignore _types only
function Command(_ref) {
var Command = /*#__PURE__*/(0, _createClass2["default"])(function Command(_ref) {
var commandId = _ref.commandId,

@@ -33,0 +32,0 @@ requiredEventStores = _ref.requiredEventStores,

@@ -16,4 +16,3 @@ "use strict";

var EventType = /*#__PURE__*/(0, _createClass2["default"])( // @ts-ignore _types only
function EventType(_ref) {
var EventType = /*#__PURE__*/(0, _createClass2["default"])(function EventType(_ref) {
var type = _ref.type;

@@ -20,0 +19,0 @@ (0, _classCallCheck2["default"])(this, EventType);

@@ -34,4 +34,3 @@ "use strict";

var EventStore = /*#__PURE__*/(0, _createClass2["default"])( // @ts-ignore _types only
var EventStore = /*#__PURE__*/(0, _createClass2["default"])(
/**

@@ -38,0 +37,0 @@ * @debt v2 "rename as eventTypes"

@@ -15,4 +15,3 @@ import _createClass from "@babel/runtime/helpers/createClass";

export var Command = /*#__PURE__*/_createClass( // @ts-ignore _types only
function Command(_ref) {
export var Command = /*#__PURE__*/_createClass(function Command(_ref) {
var commandId = _ref.commandId,

@@ -19,0 +18,0 @@ requiredEventStores = _ref.requiredEventStores,

import _createClass from "@babel/runtime/helpers/createClass";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
export var EventType = /*#__PURE__*/_createClass( // @ts-ignore _types only
function EventType(_ref) {
export var EventType = /*#__PURE__*/_createClass(function EventType(_ref) {
var type = _ref.type;

@@ -7,0 +6,0 @@

@@ -17,4 +17,3 @@ import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";

import { validateSnapshotInterval } from "./utils/validateSnapshotInterval";
export var EventStore = /*#__PURE__*/_createClass( // @ts-ignore _types only
export var EventStore = /*#__PURE__*/_createClass(
/**

@@ -21,0 +20,0 @@ * @debt v2 "rename as eventTypes"

import type { EventStore } from "../eventStore";
import type { $Contravariant } from "../utils";
export declare const tuple: <A extends unknown[]>(...args: A) => A;
export declare class Command<$E extends EventStore[] = EventStore[], E extends EventStore[] = EventStore[] extends $E ? any : $E, I = any, O = any> {
_types: {
export declare class Command<E extends EventStore[] = EventStore[], $E extends EventStore[] = $Contravariant<E, EventStore[]>, I = any, O = any> {
_types?: {
input: I;

@@ -9,10 +10,10 @@ output: O;

commandId: string;
requiredEventStores: $E;
handler: (input: I, requiredEventStores: E) => Promise<O>;
requiredEventStores: E;
handler: (input: I, requiredEventStores: $E) => Promise<O>;
constructor({ commandId, requiredEventStores, handler, }: {
commandId: string;
requiredEventStores: $E;
handler: (input: I, requiredEventStores: E) => Promise<O>;
requiredEventStores: E;
handler: (input: I, requiredEventStores: $E) => Promise<O>;
});
}
//# sourceMappingURL=command.d.ts.map

@@ -1,9 +0,10 @@

export declare type EventDetail = {
import { O } from 'ts-toolbelt';
export declare type EventDetail<T extends string = string, P = unknown, M = unknown> = O.Optional<O.Omit<{
aggregateId: string;
version: number;
type: string;
type: T;
timestamp: string;
payload?: unknown;
metadata?: unknown;
};
payload: P;
metadata: M;
}, ([P] extends [never] ? 'payload' : never) | ([M] extends [never] ? 'metadata' : never)>, (undefined extends P ? 'payload' : never) | (undefined extends M ? 'metadata' : never)>;
//# sourceMappingURL=eventDetail.d.ts.map
import { EventDetail } from './eventDetail';
export declare class EventType<T extends string = string, D extends EventDetail = EventDetail> {
_types: {
detail: D;
export declare class EventType<T extends string = string, P = string extends T ? unknown : never, M = string extends T ? unknown : never> {
_types?: {
detail: EventDetail<T, P, M>;
};

@@ -11,4 +11,4 @@ type: T;

}
export declare type EventTypeDetail<E extends EventType> = E['_types']['detail'];
export declare type EventTypeDetail<E extends EventType> = NonNullable<E['_types']>['detail'];
export declare type EventTypesDetails<E extends EventType[]> = E[number] extends infer U ? U extends EventType ? EventTypeDetail<U> : never : never;
//# sourceMappingURL=eventType.d.ts.map

@@ -5,5 +5,6 @@ import type { Aggregate } from "../aggregate";

import type { StorageAdapter } from "../storageAdapter";
import { AggregateIdsLister, EventPusher, EventsGetter, SideEffectsSimulator, AggregateGetter, AggregateSimulator } from './types';
export declare class EventStore<I extends string = string, E extends EventType[] = EventType[], D extends EventDetail = EventTypesDetails<E>, $D extends EventDetail = EventDetail extends D ? any : D, R extends (aggregate: any, event: $D) => Aggregate = (aggregate: any, event: $D) => Aggregate, A extends Aggregate = ReturnType<R>, $A extends Aggregate = Aggregate extends A ? any : A> {
_types: {
import type { $Contravariant } from "../utils";
import { AggregateIdsLister, EventPusher, EventsGetter, SideEffectsSimulator, AggregateGetter, AggregateSimulator, Reducer } from './types';
export declare class EventStore<I extends string = string, E extends EventType[] = EventType[], D extends EventDetail = EventTypesDetails<E>, $D extends EventDetail = $Contravariant<D, EventDetail>, R extends Reducer<Aggregate, $D> = Reducer<Aggregate, $D>, A extends Aggregate = ReturnType<R>, $A extends Aggregate = $Contravariant<A, Aggregate>> {
_types?: {
details: D;

@@ -10,0 +11,0 @@ aggregate: A;

@@ -12,20 +12,5 @@ /// <reference types="jest" />

export declare const mockStorageAdapter: StorageAdapter;
export declare const counterCreatedEvent: EventType<"COUNTER_CREATED", {
aggregateId: string;
version: number;
type: 'COUNTER_CREATED';
timestamp: string;
}>;
export declare const counterIncrementedEvent: EventType<"COUNTER_INCREMENTED", {
aggregateId: string;
version: number;
type: 'COUNTER_INCREMENTED';
timestamp: string;
}>;
export declare const counterDeletedEvent: EventType<"COUNTER_DELETED", {
aggregateId: string;
version: number;
type: 'COUNTER_DELETED';
timestamp: string;
}>;
export declare const counterCreatedEvent: EventType<"COUNTER_CREATED", never, never>;
export declare const counterIncrementedEvent: EventType<"COUNTER_INCREMENTED", never, never>;
export declare const counterDeletedEvent: EventType<"COUNTER_DELETED", never, never>;
export declare type CounterEventsDetails = EventTypeDetail<typeof counterCreatedEvent> | EventTypeDetail<typeof counterIncrementedEvent> | EventTypeDetail<typeof counterDeletedEvent>;

@@ -43,26 +28,11 @@ export declare type CounterAggregate = {

export declare const countersReducer: (counterAggregate: CounterAggregate, event: CounterEventsDetails) => CounterAggregate;
export declare const counterEventStore: EventStore<"Counters", (EventType<"COUNTER_CREATED", {
export declare const counterEventStore: EventStore<"Counters", (EventType<"COUNTER_INCREMENTED", never, never> | EventType<"COUNTER_CREATED", never, never> | EventType<"COUNTER_DELETED", never, never>)[], {
aggregateId: string;
version: number;
type: 'COUNTER_CREATED';
type: "COUNTER_INCREMENTED";
timestamp: string;
}> | EventType<"COUNTER_INCREMENTED", {
aggregateId: string;
version: number;
type: 'COUNTER_INCREMENTED';
timestamp: string;
}> | EventType<"COUNTER_DELETED", {
aggregateId: string;
version: number;
type: 'COUNTER_DELETED';
timestamp: string;
}>)[], {
aggregateId: string;
version: number;
type: 'COUNTER_CREATED';
timestamp: string;
} | {
aggregateId: string;
version: number;
type: 'COUNTER_INCREMENTED';
type: "COUNTER_CREATED";
timestamp: string;

@@ -72,3 +42,3 @@ } | {

version: number;
type: 'COUNTER_DELETED';
type: "COUNTER_DELETED";
timestamp: string;

@@ -78,3 +48,3 @@ }, {

version: number;
type: 'COUNTER_CREATED';
type: "COUNTER_INCREMENTED";
timestamp: string;

@@ -84,3 +54,3 @@ } | {

version: number;
type: 'COUNTER_INCREMENTED';
type: "COUNTER_CREATED";
timestamp: string;

@@ -90,21 +60,10 @@ } | {

version: number;
type: 'COUNTER_DELETED';
type: "COUNTER_DELETED";
timestamp: string;
}, (counterAggregate: CounterAggregate, event: CounterEventsDetails) => CounterAggregate, CounterAggregate, CounterAggregate>;
export declare const userCreatedEvent: EventType<"USER_CREATED", {
aggregateId: string;
version: number;
type: 'USER_CREATED';
timestamp: string;
payload: {
name: string;
age: number;
};
}>;
export declare const userRemovedEvent: EventType<"USER_REMOVED", {
aggregateId: string;
version: number;
type: 'USER_REMOVED';
timestamp: string;
}>;
name: string;
age: number;
}, never>;
export declare const userRemovedEvent: EventType<"USER_REMOVED", never, never>;
export declare type UserEventsDetails = EventTypeDetail<typeof userCreatedEvent> | EventTypeDetail<typeof userRemovedEvent>;

@@ -122,5 +81,8 @@ export declare type UserAggregate = {

export declare const userEventStore: EventStore<"Users", (EventType<"USER_CREATED", {
name: string;
age: number;
}, never> | EventType<"USER_REMOVED", never, never>)[], {
aggregateId: string;
version: number;
type: 'USER_CREATED';
type: "USER_CREATED";
timestamp: string;

@@ -131,20 +93,6 @@ payload: {

};
}> | EventType<"USER_REMOVED", {
aggregateId: string;
version: number;
type: 'USER_REMOVED';
timestamp: string;
}>)[], {
aggregateId: string;
version: number;
type: 'USER_CREATED';
timestamp: string;
payload: {
name: string;
age: number;
};
} | {
aggregateId: string;
version: number;
type: 'USER_REMOVED';
type: "USER_REMOVED";
timestamp: string;

@@ -154,3 +102,3 @@ }, {

version: number;
type: 'USER_CREATED';
type: "USER_CREATED";
timestamp: string;

@@ -164,5 +112,5 @@ payload: {

version: number;
type: 'USER_REMOVED';
type: "USER_REMOVED";
timestamp: string;
}, (userAggregate: UserAggregate, event: UserEventsDetails) => UserAggregate, UserAggregate, UserAggregate>;
//# sourceMappingURL=eventStore.util.test.d.ts.map
import { EventStore } from './eventStore';
export declare type EventStoreId<E extends EventStore> = E['eventStoreId'];
export declare type EventStoreEventsTypes<E extends EventStore> = E['eventStoreEvents'];
export declare type EventStoreEventsDetails<E extends EventStore> = E['_types']['details'];
export declare type EventStoreEventsDetails<E extends EventStore> = NonNullable<E['_types']>['details'];
export declare type EventStoreReducer<E extends EventStore> = E['reduce'];
export declare type EventStoreAggregate<E extends EventStore> = E['_types']['aggregate'];
export declare type EventStoreAggregate<E extends EventStore> = NonNullable<E['_types']>['aggregate'];
//# sourceMappingURL=generics.d.ts.map

@@ -1,5 +0,7 @@

import { Aggregate } from "../aggregate";
import { EventDetail } from "../event/eventDetail";
import { EventsQueryOptions, ListAggregateIdsOptions, ListAggregateIdsOutput } from "../storageAdapter";
export declare type SideEffectsSimulator<D extends EventDetail, $D extends EventDetail> = (indexedEvents: Record<string, Omit<$D, 'version'>>, event: $D) => Record<string, Omit<D, 'version'>>;
import type { Aggregate } from "../aggregate";
import type { EventDetail } from "../event/eventDetail";
import type { EventsQueryOptions, ListAggregateIdsOptions, ListAggregateIdsOutput } from "../storageAdapter";
import type { $Contravariant } from "../utils";
export declare type Reducer<A extends Aggregate = Aggregate, D extends EventDetail = EventDetail, $D extends EventDetail = $Contravariant<D, EventDetail>, $A extends Aggregate = $Contravariant<A, Aggregate>> = (aggregate: $A, event: $D) => A;
export declare type SideEffectsSimulator<D extends EventDetail, $D extends EventDetail = $Contravariant<D, EventDetail>> = (indexedEvents: Record<string, Omit<$D, 'version'>>, event: $D) => Record<string, Omit<D, 'version'>>;
export declare type EventsGetter<D extends EventDetail> = (aggregateId: string, options?: EventsQueryOptions) => Promise<{

@@ -6,0 +8,0 @@ events: D[];

@@ -11,4 +11,5 @@ export type { Aggregate } from './aggregate';

export { EventStore } from './eventStore';
export type { SimulationOptions, EventStoreId, EventStoreEventsTypes, EventStoreEventsDetails, EventStoreReducer, EventStoreAggregate, } from './eventStore';
export type { SimulationOptions, EventStoreId, EventStoreEventsTypes, EventStoreEventsDetails, EventStoreReducer, EventStoreAggregate, Reducer, } from './eventStore';
export { Command, tuple } from './command/command';
export type { $Contravariant } from './utils';
//# sourceMappingURL=index.d.ts.map

@@ -76,3 +76,3 @@ {

},
"version": "1.3.1"
"version": "1.4.0"
}

@@ -17,104 +17,271 @@ <p align="center">

# Better DevX for Event Sourcing in TypeScript
# Making Event Sourcing easy 😎
Castore provides a unified interface for implementing Event Sourcing in TypeScript πŸ¦Έβ€β™‚οΈ.
Event Sourcing is a data storage paradigm that saves **changes in your application state** rather than the state itself. It is powerful but tricky to implement.
## πŸ€” Why use Castore ?
<!-- TODO: SCHEMA OF EVENT SOURCING -->
- πŸ’¬ **Verbosity**: Castore classes are designed to increase dryness and provide the optimal developer experience. Event Sourcing is hard, don't make it harder!
After years of using it at [Kumo](https://dev.to/kumo), we have grown to love it, but also experienced first-hand the lack of consensus and tooling around it. That's where Castore comes from!
- πŸ“ **Strong typings**: We love type inference, we know you will to!
- πŸ„β€β™‚οΈ **Interfaces before implementations**: Castore provides a standard interface to modelize common event sourcing patterns in TypeScript. But it **DOES NOT enforce any particular implementation** (storage service, messaging system etc...). You can use Castore in React apps, containers or lambdas, it's up to you! Some common implementations are provided, but you are free to use **any implementation you want** via custom classes, as long as they follow the required interfaces.
<p align="center">
Castore is a TypeScript library that <b>makes Event Sourcing easy</b> 😎
</p>
- πŸ‘ **Enforces best practices**: Gained from years of usage like using integer versions instead of timestamps, transactions for multi-store events and state-carrying transfer events for projections.
- πŸ›  **Rich suite of helpers**: Like mock events builder to help you write tests.
With Castore, you'll be able to:
- Define your [event stores](#eventstore)
- Fetch and push new [events](#events) seamlessly
- Implement and test your [commands](#command)
- ...and much more!
All that with first-class developer experience and minimal boilerplate ✨
<!-- > Castore is still under active development. A v1 has been released with a first set of features, but we have big plans for the future (read models, events replay, migrations...)! So πŸ“£ STAY TUNED πŸ“£ -->
## πŸ«€ Core Design
Some important decisions that we've made early on:
### πŸ’­ **Abstractions first**
Castore has been designed with **flexibility** in mind. It gives you abstractions that are meant to be used **anywhere**: React apps, containers, Lambdas... you name it!
For instance, `EventStore` classes are **stack agnostic**: They need an `EventStorageAdapter` class to interact with actual data. You can code your own `EventStorageAdapter` (simply implement the interface), but it's much simpler to use an off-the-shelf adapter like [`DynamoDBEventStorageAdapter`](./packages/dynamodb-event-storage-adapter/README.md).
### πŸ™…β€β™‚οΈ **We do NOT deploy resources**
While some packages like `DynamoDBEventStorageAdapter` require compatible infrastructure, Castore is not responsible for deploying it.
Though that is not something we exclude in the future, we are a small team and decided to focus on DevX first.
### β›‘ **Full type safety**
Speaking of DevX, we absolutely love TypeScript! If you do too, you're in the right place: We push type-safety to the limit in everything we do!
If you don't, that's fine πŸ‘ Castore is still available in Node/JS. And you can still profit from some nice JSDocs!
### πŸ“– **Best practices**
The Event Sourcing journey has many hidden pitfalls. We ran into them for you!
Castore is opiniated. It comes with a collection of best practices and documented anti-patterns that we hope will help you out!
## Table of content
- [Events](#events)
- [Event Store](#event-store)
- [Reducer](#reducer)
- [Storage Adapter](#storage-adapter)
- [Event Store Interface](#event-store-interface)
- [Going Further](#going-further-πŸƒβ€β™‚οΈ)
- [Getting Started](#getting-started)
- [πŸ“₯ Installation](#-installation)
- [πŸ“¦ Packages structure](#-packages-structure)
- [The Basics](#the-basics)
- [πŸ“š Events](#-events)
- [🏷 Event Types](#-eventtype)
- [πŸ— Aggregates](#-aggregate)
- [βš™οΈ Reducers](#%EF%B8%8F-reducer)
- [🎁 Event Store](#%EF%B8%8F-reducers)
- [πŸ’Ύ Event Storage Adapter](#-eventstorageadapter)
- [πŸ“¨ Command](#-command)
- [πŸ“Έ Snapshots](#-snapshots)
- [Resources](#resources)
- [🎯 Test Tools](#-test-tools)
- [πŸ”— Packages List](#-packages-list)
- [πŸ“– Common Patterns](#-common-patterns)
## Events
## Getting Started
The first step in your ✨ Castore journey ✨ is to define your business events! 🦫
### πŸ“₯ Installation
Castore lets you easily create the Event Types which will constitute your Event Store.
Simply use the EventType class and start defining, once and for all, your events! πŸŽ‰
```bash
# npm
npm install @castore/core
# yarn
yarn add @castore/core
```
### πŸ“¦ Packages structure
Castore is not a single package, but a **collection of packages** revolving around a `core` package. This is made so every line of code added to your project is _opt-in_, wether you use tree-shaking or not.
Castore packages are **released together**. Though different versions may be compatible, you are **guaranteed** to have working code as long as you use matching versions.
Here is an example of working `package.json`:
```js
{
...
"dependencies": {
"@castore/core": "1.3.1",
"@castore/dynamodb-event-storage-adapter": "1.3.1"
...
},
"devDependencies": {
"@castore/test-tools": "1.3.1"
...
}
}
```
## The Basics
### πŸ“š `Events`
Event Sourcing is all about **saving changes in your application state**. Such changes are represented by **events**, and needless to say, they are quite important πŸ™ƒ
Events that concern the same business entity (like a `User`) are aggregated through a common id called `aggregateId` (and vice versa, events that have the same `aggregateId` represent changes of the same business entity). The index of an event in such a serie of events is called its `version`.
In Castore, stored events (also called **event details**) always have exactly the following attributes:
- <code>aggregateId <i>(string)</i></code>
- <code>version <i>(integer β‰₯ 1)</i></code>
- <code>timestamp <i>(string)</i></code>: A date in ISO 8601 format
- <code>type <i>(string)</i></code>: A string identifying the business meaning of the event
- <code>payload <i>(?any = never)</i></code>: A payload of any type
- <code>metadata <i>(?any = never)</i></code>: Some metadata of any type
```ts
import { EventType } from "@castore/core"
import type { EventDetail } from '@castore/core';
export const userCreatedEvent = new EventType<
// Typescript EventType
type UserCreatedEventDetail = EventDetail<
'USER_CREATED',
// Typescript EventDetails
{
aggregateId: string;
version: number;
type: 'USER_CREATED';
timestamp: string;
payload: { name: string; age: number };
}
>({
// EventType
type: 'USER_CREATED',
});
{ name: string; age: number },
{ invitedBy?: string }
>;
const userRemovedEvent = ...
// πŸ‘‡ Equivalent to:
type UserCreatedEventDetail = {
aggregateId: string;
version: number;
timestamp: string;
type: 'USER_CREATED';
payload: { name: string; age: number };
metadata: { invitedBy?: string };
};
```
const eventTypes = [
userCreatedEvent,
userRemovedEvent,
];
### 🏷 `EventType`
Events are generally classified in **events types** (not to confuse with TS types). Castore lets you declare them via the `EventType` class:
```ts
import { EventType } from '@castore/core';
export const userCreatedEventType = new EventType<
'USER_CREATED',
{ name: string; age: number },
{ invitedBy?: string }
>({ type: 'USER_CREATED' });
```
> You can also define your events with JSON Schemas or Zod Events, see `@castore/json-schema-event` and `@castore/zod-event` documentations for implementation 🦫
Note that we only provide TS types for `payload` and `metadata` properties. That is because, as stated in the [core design](#πŸ«€-core-design), **Castore is meant to be as flexible as possible**, and that includes the validation library you want to use: The `EventType` class is not meant to be used directly, but rather extended by other classes which will add run-time validation methods to it πŸ‘
Once you're happy with your set of EventTypes you can move on to step 2: attaching the EventTypes to an actual EventStore! πŸͺ.
See the following packages for examples:
## Event Store
- [JSON-Schema Event Type](./packages/json-schema-event/README.md)
- [Zod Event Type](./packages/zod-event/README.md)
Welcome in the heart of Castore: the EventStore ❀️<br/>
The `EventStore` class lets you instantiate an object containing all the methods you will need to interact with your event sourcing store. πŸ’ͺ
**Constructor:**
```typescript
const userEventStore = new EventStore({
eventStoreId: 'user-event-store-id',
eventTypes,
// πŸ‘‡ See #reducer sub-section
reducer,
// πŸ‘‡ See #storage_adapters section
storageAdapter,
});
- <code>type <i>(string)</i></code>: The event type
```ts
import { EventType } from '@castore/core';
export const userCreatedEventType = new EventType({ type: 'USER_CREATED' });
```
### Reducer
**Properties:**
The reducer needed in the EventStore initialization is the function that will be applied to the sorted array of events in order to build the aggregates βš™οΈ. It works like your usual Redux reducer!
- <code>type <i>(string)</i>:</code> The event type
Basically, it consists in a function implementing switch cases for all event types and returning the aggregate updated with your business logic. 🧠
```ts
const eventType = userCreatedEventType.type;
// => "USER_CREATED"
```
Here is an example reducer for our User Event Store.
**Type Helpers:**
- <code>EventTypeDetail</code>: Returns the event detail TS type of an `EventType`
```ts
export const usersReducer = (
userAggregate: UserAggregate,
event: UserEventsDetails,
): UserAggregate => {
const { version, aggregateId } = event;
import type { EventTypeDetail } from '@castore/core';
switch (event.type) {
type UserCreatedEventTypeDetail = EventTypeDetail<typeof userCreatedEventType>;
// πŸ‘‡ Equivalent to:
type UserCreatedEventTypeDetail = {
aggregateId: string;
version: number;
timestamp: string;
type: 'USER_CREATED';
payload: { name: string; age: number };
metadata: { invitedBy?: string };
};
```
- <code>EventTypesDetails</code>: Return the events details of a list of `EventType`
```ts
import type { EventTypesDetails } from '@castore/core';
type UserEventTypesDetails = EventTypesDetails<
[typeof userCreatedEventType, typeof userRemovedEventType]
>;
// => EventTypeDetail<typeof userCreatedEventType>
// | EventTypeDetail<typeof userRemovedEventType>
```
### πŸ— `Aggregate`
Eventhough entities are stored as series of events, we still want to use a **stable interface to represent their states at a point in time** rather than directly using events. In Castore, it is implemented by a TS type called `Aggregate`.
> ☝️ Think of aggregates as _"what the data would look like in CRUD"_
In Castore, aggregates necessarily contain an `aggregateId` and `version` attributes (the `version` of the latest `event`). But for the rest, it's up to you πŸ€·β€β™‚οΈ
For instance, we can include a `name`, `age` and `status` properties to our `UserAggregate`:
```ts
import type { Aggregate } from '@castore/core';
// Represents a User at a point in time
interface UserAggregate extends Aggregate {
name: string;
age: number;
status: 'CREATED' | 'REMOVED';
}
// πŸ‘‡ Equivalent to:
interface UserAggregate {
aggregateId: string;
version: number;
name: string;
age: number;
status: 'CREATED' | 'REMOVED';
}
```
### βš™οΈ `Reducer`
Aggregates are derived from their events by [reducing them](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) through a `reducer` function. It defines **how to update the aggregate when a new event is pushed**:
<!-- TODO: SCHEMA OF EVENTS AGGREGATE -->
```ts
import type { Reducer } from '@castore/core';
export const usersReducer: Reducer<UserAggregate, UserEventsDetails> = (
userAggregate,
newEvent,
) => {
const { version, aggregateId } = newEvent;
switch (newEvent.type) {
case 'USER_CREATED': {
const { name, age } = event.payload;
const { name, age } = newEvent.payload;
// πŸ‘‡ Return the next version of the aggregate
return {
aggregateId,
version: event.version,
version,
name,

@@ -126,73 +293,127 @@ age,

case 'USER_REMOVED':
return {
...userAggregate,
version,
status: 'REMOVED',
};
return { ...userAggregate, version, status: 'REMOVED' };
}
};
const johnDowAggregate: UserAggregate = johnDowEvents.reduce(usersReducer);
```
### Storage Adapter
> ☝️ Note that aggregates are always **computed on the fly**, and NOT stored. Changing them does not require any data migration whatsoever (except if you use snapshots, an invalidation is needed first).
!['Storage Adapter'](/assets/storage_adapter_schema.png)
### 🎁 `EventStore`
You can store your events in many different ways. To specify how to store them (in memory, DynamoDB...) Castore implements Storage Adapters.
Once you've defined your [event types](#-eventtype) and how to [aggregate](#%EF%B8%8F-reducer) them, you can bundle them together in an `EventStore` class.
Adapters offer an interface between the Event Store class and your storage method πŸ’Ύ.
Each event store in your application represents a business entity. Think of event stores as _"what tables would be in CRUD"_, except that instead of directly updating data, you just append new events to it!
To be able to use your EventStore, you will need to attach a Storage Adapter πŸ”—.
In Castore, `EventStore` classes are NOT responsible for actually storing data (this will come with [event storage adapters](#-eventstorageadapter)). But rather to provide a boilerplate-free and type-safe interface to perform many actions such as:
- Listing aggregate ids
- Accessing events of an aggregate
- Building an aggregate with the reducer
- Pushing new events etc...
All the Storage Adapters have the same interface, and you can create your own if you want to implement new storage methods!
```ts
import { EventStore } from '@castore/core';
So far, castore supports 2 Storage Adapters ✨:
const userEventStore = new EventStore({
eventStoreId: 'USERS',
eventTypes: [
userCreatedEventType,
userRemovedEventType,
...
],
reducer: usersReducer,
});
// ...and that's it πŸ₯³
```
- in-memory
- DynamoDB
> ☝️ The `EventStore` class is the heart of Castore, it even gave it its name!
### Event Store Interface
**Constructor:**
Now that our Event Store has been instantiated with a reducer and a Storage Adapter, we can start using it to actually populate our database with events and retrieve business data from it 🌈.
_...coming soon_
To do that, the Event Store class exposes several methods, including the following two:
<!-- > ☝️ Note that it's the ReturnType of the `reducer` function that is used to infer the `Aggregate` type of the EventStore. -->
- `pushEvent`: Takes an object containing event details and puts it in the database. It will throw if the event's version already exists!
**Properties:**
- `getAggregate`: Returns the output of the reducer applied to the array of all events.
_...coming soon_
Here is a quick example showing how an application would use these two methods:
**Type Helpers:**
_...coming soon_
<!-- EventStoreId
```ts
const removeUser = async (userId: string) => {
// get the aggregate for that userId,
// which is a representation of our user's state
const { aggregate } = await userEventStore.getAggregate(userId);
import type { EventStoreId } from '@castore/core';
// use the aggregate to check the user status
if (aggregate.status === 'REMOVED') {
throw new Error('User already removed');
}
type UserEventStoreId = EventStoreId<typeof userEventStore>;
// => "USERS"
```
// put the USER_REMOVED event in the event store 🦫
await userEventStore.pushEvent({
aggregateId: userId,
version: aggregate.version + 1,
type: 'USER_REMOVED',
timestamp: new Date(),
});
};
EventStoreEventsTypes
```ts
import type { EventStoreEventsTypes } from '@castore/core';
type UserEventsTypes = EventStoreEventsTypes<typeof userEventStore>;
// => [typeof userCreatedEventTypeType, typeof userRemovedEventTypeType...]
```
## Going Further πŸƒβ€β™‚οΈ
EventStoreEventsDetails
We've only covered the basic functionalities of the Event Store!
```ts
import type { EventStoreEventsDetails } from '@castore/core';
The Event Store class actually implements other very useful methods πŸ’ͺ
type UserEventsDetails = EventStoreEventsTypes<typeof userEventStore>;
// => TODO
```
Here is a small recap of these methods:
EventStoreEventsDetails
- `getEvents`: Returns the list of all events for a given aggregateId.
```ts
import type { EventStoreEventsDetails } from '@castore/core';
- `listAggregateIds`: Returns the list of all aggregateIds present in the Event Store.
type UserEventsDetails = EventStoreEventsTypes<typeof userEventStore>;
``` -->
- `simulateAggregate`: Simulates the aggregate you would have obtained with getAggregate at a given date.
### πŸ’Ύ `EventStorageAdapter`
_...coming soon_
<!-- You can store your events in many different ways. To specify how to store them (in memory, DynamoDB...) Castore implements Storage Adapters.
Adapters offer an interface between the Event Store class and your storage method πŸ’Ύ.
To be able to use your EventStore, you will need to attach a Storage Adapter πŸ”—.
All the Storage Adapters have the same interface, and you can create your own if you want to implement new storage methods!
So far, castore supports 2 Storage Adapters ✨:
- in-memory
- DynamoDB -->
### πŸ“¨ `Command`
_...coming soon_
### πŸ“Έ `Snapshots`
_...coming soon_
## Resources
### 🎯 Test Tools
_...coming soon_
### πŸ”— Packages List
_...coming soon_
### πŸ“– Common Patterns
_...coming soon_

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚑️ by Socket Inc