@castore/core
Advanced tools
Comparing version 1.4.0 to 1.4.1
@@ -26,4 +26,2 @@ "use strict"; | ||
var _validateSnapshotInterval = require("./utils/validateSnapshotInterval"); | ||
var _excluded = ["aggregate", "lastEvent"]; | ||
@@ -57,5 +55,3 @@ | ||
} : _ref$simulateSideEffe, | ||
$storageAdapter = _ref.storageAdapter, | ||
_ref$snapshotInterval = _ref.snapshotInterval, | ||
snapshotInterval = _ref$snapshotInterval === void 0 ? Infinity : _ref$snapshotInterval; | ||
$storageAdapter = _ref.storageAdapter; | ||
(0, _classCallCheck2["default"])(this, EventStore); | ||
@@ -67,3 +63,2 @@ (0, _defineProperty2["default"])(this, "_types", void 0); | ||
(0, _defineProperty2["default"])(this, "simulateSideEffect", void 0); | ||
(0, _defineProperty2["default"])(this, "snapshotInterval", void 0); | ||
(0, _defineProperty2["default"])(this, "getEvents", void 0); | ||
@@ -87,3 +82,2 @@ (0, _defineProperty2["default"])(this, "pushEvent", void 0); | ||
this.storageAdapter = $storageAdapter; | ||
this.snapshotInterval = (0, _validateSnapshotInterval.validateSnapshotInterval)(snapshotInterval); | ||
@@ -127,4 +121,3 @@ this.getStorageAdapter = function () { | ||
var _ref3 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(eventDetail) { | ||
var storageAdapter, version, aggregateId, _yield$_this$getAggre, _aggregate; | ||
var storageAdapter; | ||
return _regenerator["default"].wrap(function _callee2$(_context2) { | ||
@@ -141,29 +134,2 @@ while (1) { | ||
case 3: | ||
version = eventDetail.version, aggregateId = eventDetail.aggregateId; | ||
if (!(version % _this.snapshotInterval === 0)) { | ||
_context2.next = 14; | ||
break; | ||
} | ||
_context2.next = 7; | ||
return _this.getAggregate(aggregateId); | ||
case 7: | ||
_yield$_this$getAggre = _context2.sent; | ||
_aggregate = _yield$_this$getAggre.aggregate; | ||
if (_aggregate) { | ||
_context2.next = 12; | ||
break; | ||
} | ||
console.error('Unable to create snapshot: Aggregate not found'); | ||
return _context2.abrupt("return"); | ||
case 12: | ||
_context2.next = 14; | ||
return storageAdapter.putSnapshot(_aggregate); | ||
case 14: | ||
case "end": | ||
@@ -208,5 +174,4 @@ return _context2.stop(); | ||
var _ref5 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee4(aggregateId) { | ||
var options, | ||
var _ref6, | ||
maxVersion, | ||
snapshot, | ||
_yield$_this$getEvent, | ||
@@ -222,28 +187,12 @@ events, | ||
case 0: | ||
options = _args4.length > 1 && _args4[1] !== undefined ? _args4[1] : {}; | ||
maxVersion = options.maxVersion; | ||
if (!(maxVersion === undefined || maxVersion >= _this.snapshotInterval)) { | ||
_context4.next = 6; | ||
break; | ||
} | ||
_context4.next = 5; | ||
return _this.getStorageAdapter().getLastSnapshot(aggregateId, { | ||
_ref6 = _args4.length > 1 && _args4[1] !== undefined ? _args4[1] : {}, maxVersion = _ref6.maxVersion; | ||
_context4.next = 3; | ||
return _this.getEvents(aggregateId, { | ||
maxVersion: maxVersion | ||
}); | ||
case 5: | ||
snapshot = _context4.sent.snapshot; | ||
case 6: | ||
_context4.next = 8; | ||
return _this.getEvents(aggregateId, _objectSpread(_objectSpread({}, options), {}, { | ||
minVersion: snapshot ? snapshot.version + 1 : undefined | ||
})); | ||
case 8: | ||
case 3: | ||
_yield$_this$getEvent = _context4.sent; | ||
events = _yield$_this$getEvent.events; | ||
aggregate = _this.buildAggregate(events, snapshot); | ||
aggregate = _this.buildAggregate(events, undefined); | ||
lastEvent = events[events.length - 1]; | ||
@@ -253,7 +202,6 @@ return _context4.abrupt("return", { | ||
events: events, | ||
lastEvent: lastEvent, | ||
snapshot: snapshot | ||
lastEvent: lastEvent | ||
}); | ||
case 13: | ||
case 8: | ||
case "end": | ||
@@ -272,4 +220,4 @@ return _context4.stop(); | ||
this.getExistingAggregate = /*#__PURE__*/function () { | ||
var _ref6 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee5(aggregateId, options) { | ||
var _yield$_this$getAggre2, aggregate, lastEvent, restAggregate; | ||
var _ref7 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee5(aggregateId, options) { | ||
var _yield$_this$getAggre, aggregate, lastEvent, restAggregate; | ||
@@ -284,6 +232,6 @@ return _regenerator["default"].wrap(function _callee5$(_context5) { | ||
case 2: | ||
_yield$_this$getAggre2 = _context5.sent; | ||
aggregate = _yield$_this$getAggre2.aggregate; | ||
lastEvent = _yield$_this$getAggre2.lastEvent; | ||
restAggregate = (0, _objectWithoutProperties2["default"])(_yield$_this$getAggre2, _excluded); | ||
_yield$_this$getAggre = _context5.sent; | ||
aggregate = _yield$_this$getAggre.aggregate; | ||
lastEvent = _yield$_this$getAggre.lastEvent; | ||
restAggregate = (0, _objectWithoutProperties2["default"])(_yield$_this$getAggre, _excluded); | ||
@@ -315,3 +263,3 @@ if (!(aggregate === undefined || lastEvent === undefined)) { | ||
return function (_x6, _x7) { | ||
return _ref6.apply(this, arguments); | ||
return _ref7.apply(this, arguments); | ||
}; | ||
@@ -321,4 +269,4 @@ }(); | ||
this.simulateAggregate = function (events) { | ||
var _ref7 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
simulationDate = _ref7.simulationDate; | ||
var _ref8 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
simulationDate = _ref8.simulationDate; | ||
@@ -328,4 +276,4 @@ var eventsWithSideEffects = Object.values(events.reduce(_this.simulateSideEffect, {})); | ||
if (simulationDate !== undefined) { | ||
eventsWithSideEffects = eventsWithSideEffects.filter(function (_ref8) { | ||
var timestamp = _ref8.timestamp; | ||
eventsWithSideEffects = eventsWithSideEffects.filter(function (_ref9) { | ||
var timestamp = _ref9.timestamp; | ||
return timestamp <= simulationDate; | ||
@@ -335,5 +283,5 @@ }); | ||
var sortedEventsWithSideEffects = eventsWithSideEffects.sort(function (_ref9, _ref10) { | ||
var timestampA = _ref9.timestamp; | ||
var timestampB = _ref10.timestamp; | ||
var sortedEventsWithSideEffects = eventsWithSideEffects.sort(function (_ref10, _ref11) { | ||
var timestampA = _ref10.timestamp; | ||
var timestampB = _ref11.timestamp; | ||
return timestampA < timestampB ? -1 : 1; | ||
@@ -340,0 +288,0 @@ }).map(function (event, index) { |
@@ -16,3 +16,2 @@ import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; | ||
import { UndefinedStorageAdapterError } from "../errors/undefinedStorageAdapterError"; | ||
import { validateSnapshotInterval } from "./utils/validateSnapshotInterval"; | ||
export var EventStore = /*#__PURE__*/_createClass( | ||
@@ -40,5 +39,3 @@ /** | ||
} : _ref$simulateSideEffe, | ||
$storageAdapter = _ref.storageAdapter, | ||
_ref$snapshotInterval = _ref.snapshotInterval, | ||
snapshotInterval = _ref$snapshotInterval === void 0 ? Infinity : _ref$snapshotInterval; | ||
$storageAdapter = _ref.storageAdapter; | ||
@@ -57,4 +54,2 @@ _classCallCheck(this, EventStore); | ||
_defineProperty(this, "snapshotInterval", void 0); | ||
_defineProperty(this, "getEvents", void 0); | ||
@@ -87,3 +82,2 @@ | ||
this.storageAdapter = $storageAdapter; | ||
this.snapshotInterval = validateSnapshotInterval(snapshotInterval); | ||
@@ -127,4 +121,3 @@ this.getStorageAdapter = function () { | ||
var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(eventDetail) { | ||
var storageAdapter, version, aggregateId, _yield$_this$getAggre, _aggregate; | ||
var storageAdapter; | ||
return _regeneratorRuntime.wrap(function _callee2$(_context2) { | ||
@@ -141,29 +134,2 @@ while (1) { | ||
case 3: | ||
version = eventDetail.version, aggregateId = eventDetail.aggregateId; | ||
if (!(version % _this.snapshotInterval === 0)) { | ||
_context2.next = 14; | ||
break; | ||
} | ||
_context2.next = 7; | ||
return _this.getAggregate(aggregateId); | ||
case 7: | ||
_yield$_this$getAggre = _context2.sent; | ||
_aggregate = _yield$_this$getAggre.aggregate; | ||
if (_aggregate) { | ||
_context2.next = 12; | ||
break; | ||
} | ||
console.error('Unable to create snapshot: Aggregate not found'); | ||
return _context2.abrupt("return"); | ||
case 12: | ||
_context2.next = 14; | ||
return storageAdapter.putSnapshot(_aggregate); | ||
case 14: | ||
case "end": | ||
@@ -208,5 +174,4 @@ return _context2.stop(); | ||
var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee4(aggregateId) { | ||
var options, | ||
var _ref6, | ||
maxVersion, | ||
snapshot, | ||
_yield$_this$getEvent, | ||
@@ -222,28 +187,12 @@ events, | ||
case 0: | ||
options = _args4.length > 1 && _args4[1] !== undefined ? _args4[1] : {}; | ||
maxVersion = options.maxVersion; | ||
if (!(maxVersion === undefined || maxVersion >= _this.snapshotInterval)) { | ||
_context4.next = 6; | ||
break; | ||
} | ||
_context4.next = 5; | ||
return _this.getStorageAdapter().getLastSnapshot(aggregateId, { | ||
_ref6 = _args4.length > 1 && _args4[1] !== undefined ? _args4[1] : {}, maxVersion = _ref6.maxVersion; | ||
_context4.next = 3; | ||
return _this.getEvents(aggregateId, { | ||
maxVersion: maxVersion | ||
}); | ||
case 5: | ||
snapshot = _context4.sent.snapshot; | ||
case 6: | ||
_context4.next = 8; | ||
return _this.getEvents(aggregateId, _objectSpread(_objectSpread({}, options), {}, { | ||
minVersion: snapshot ? snapshot.version + 1 : undefined | ||
})); | ||
case 8: | ||
case 3: | ||
_yield$_this$getEvent = _context4.sent; | ||
events = _yield$_this$getEvent.events; | ||
aggregate = _this.buildAggregate(events, snapshot); | ||
aggregate = _this.buildAggregate(events, undefined); | ||
lastEvent = events[events.length - 1]; | ||
@@ -253,7 +202,6 @@ return _context4.abrupt("return", { | ||
events: events, | ||
lastEvent: lastEvent, | ||
snapshot: snapshot | ||
lastEvent: lastEvent | ||
}); | ||
case 13: | ||
case 8: | ||
case "end": | ||
@@ -272,4 +220,4 @@ return _context4.stop(); | ||
this.getExistingAggregate = /*#__PURE__*/function () { | ||
var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee5(aggregateId, options) { | ||
var _yield$_this$getAggre2, aggregate, lastEvent, restAggregate; | ||
var _ref7 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee5(aggregateId, options) { | ||
var _yield$_this$getAggre, aggregate, lastEvent, restAggregate; | ||
@@ -284,6 +232,6 @@ return _regeneratorRuntime.wrap(function _callee5$(_context5) { | ||
case 2: | ||
_yield$_this$getAggre2 = _context5.sent; | ||
aggregate = _yield$_this$getAggre2.aggregate; | ||
lastEvent = _yield$_this$getAggre2.lastEvent; | ||
restAggregate = _objectWithoutProperties(_yield$_this$getAggre2, _excluded); | ||
_yield$_this$getAggre = _context5.sent; | ||
aggregate = _yield$_this$getAggre.aggregate; | ||
lastEvent = _yield$_this$getAggre.lastEvent; | ||
restAggregate = _objectWithoutProperties(_yield$_this$getAggre, _excluded); | ||
@@ -315,3 +263,3 @@ if (!(aggregate === undefined || lastEvent === undefined)) { | ||
return function (_x6, _x7) { | ||
return _ref6.apply(this, arguments); | ||
return _ref7.apply(this, arguments); | ||
}; | ||
@@ -321,4 +269,4 @@ }(); | ||
this.simulateAggregate = function (events) { | ||
var _ref7 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
simulationDate = _ref7.simulationDate; | ||
var _ref8 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
simulationDate = _ref8.simulationDate; | ||
@@ -328,4 +276,4 @@ var eventsWithSideEffects = Object.values(events.reduce(_this.simulateSideEffect, {})); | ||
if (simulationDate !== undefined) { | ||
eventsWithSideEffects = eventsWithSideEffects.filter(function (_ref8) { | ||
var timestamp = _ref8.timestamp; | ||
eventsWithSideEffects = eventsWithSideEffects.filter(function (_ref9) { | ||
var timestamp = _ref9.timestamp; | ||
return timestamp <= simulationDate; | ||
@@ -335,5 +283,5 @@ }); | ||
var sortedEventsWithSideEffects = eventsWithSideEffects.sort(function (_ref9, _ref10) { | ||
var timestampA = _ref9.timestamp; | ||
var timestampB = _ref10.timestamp; | ||
var sortedEventsWithSideEffects = eventsWithSideEffects.sort(function (_ref10, _ref11) { | ||
var timestampA = _ref10.timestamp; | ||
var timestampB = _ref11.timestamp; | ||
return timestampA < timestampB ? -1 : 1; | ||
@@ -340,0 +288,0 @@ }).map(function (event, index) { |
@@ -16,3 +16,2 @@ import type { Aggregate } from "../aggregate"; | ||
simulateSideEffect: SideEffectsSimulator<D, $D>; | ||
snapshotInterval: number; | ||
getEvents: EventsGetter<D>; | ||
@@ -27,3 +26,3 @@ pushEvent: EventPusher<$D>; | ||
getStorageAdapter: () => StorageAdapter; | ||
constructor({ eventStoreId, eventStoreEvents, reduce, simulateSideEffect, storageAdapter: $storageAdapter, snapshotInterval, }: { | ||
constructor({ eventStoreId, eventStoreEvents, reduce, simulateSideEffect, storageAdapter: $storageAdapter, }: { | ||
eventStoreId: I; | ||
@@ -34,5 +33,4 @@ eventStoreEvents: E; | ||
storageAdapter?: StorageAdapter; | ||
snapshotInterval?: number; | ||
}); | ||
} | ||
//# sourceMappingURL=eventStore.d.ts.map |
@@ -12,7 +12,9 @@ import type { Aggregate } from "../aggregate"; | ||
export declare type AggregateIdsLister = (listAggregateOptions?: ListAggregateIdsOptions) => Promise<ListAggregateIdsOutput>; | ||
export declare type AggregateGetter<D extends EventDetail, A extends Aggregate, R extends boolean = false> = (aggregateId: string, options?: EventsQueryOptions) => Promise<{ | ||
export declare type GetAggregateOptions = { | ||
maxVersion?: number; | ||
}; | ||
export declare type AggregateGetter<D extends EventDetail, A extends Aggregate, R extends boolean = false> = (aggregateId: string, options?: GetAggregateOptions) => Promise<{ | ||
aggregate: R extends true ? A : A | undefined; | ||
events: D[]; | ||
lastEvent: R extends true ? D : D | undefined; | ||
snapshot: A | undefined; | ||
}>; | ||
@@ -19,0 +21,0 @@ export declare type SimulationOptions = { |
@@ -9,7 +9,7 @@ export type { Aggregate } from './aggregate'; | ||
export type { StorageAdapter } from './storageAdapter'; | ||
export type { EventsQueryOptions, PushEventContext, ListAggregateIdsOptions, ListAggregateIdsOutput, GetLastSnapshotOptions, ListSnapshotsOptions, } from './storageAdapter'; | ||
export type { EventsQueryOptions, PushEventContext, ListAggregateIdsOptions, ListAggregateIdsOutput, } from './storageAdapter'; | ||
export { EventStore } from './eventStore'; | ||
export type { SimulationOptions, EventStoreId, EventStoreEventsTypes, EventStoreEventsDetails, EventStoreReducer, EventStoreAggregate, Reducer, } from './eventStore'; | ||
export type { GetAggregateOptions, 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.4.0" | ||
"version": "1.4.1" | ||
} |
268
README.md
<p align="center"> | ||
<img src="assets/logo.svg" height="128"> | ||
<h1 style="border-bottom:none;font-size:60px;margin-bottom:0;" align="center" >Castore 🦫</h1> | ||
<h1 style="border-bottom:none;font-size:60px;margin-bottom:0;" align="center" >Castore</h1> | ||
</p> | ||
@@ -25,3 +25,2 @@ <p align="center"> | ||
<p align="center"> | ||
@@ -31,3 +30,2 @@ Castore is a TypeScript library that <b>makes Event Sourcing easy</b> 😎 | ||
With Castore, you'll be able to: | ||
@@ -85,3 +83,2 @@ | ||
- [📨 Command](#-command) | ||
- [📸 Snapshots](#-snapshots) | ||
- [Resources](#resources) | ||
@@ -182,3 +179,3 @@ - [🎯 Test Tools](#-test-tools) | ||
- [JSON-Schema Event Type](./packages/json-schema-event/README.md) | ||
- [JSON Schema Event Type](./packages/json-schema-event/README.md) | ||
- [Zod Event Type](./packages/zod-event/README.md) | ||
@@ -198,7 +195,7 @@ | ||
- <code>type <i>(string)</i>:</code> The event type | ||
- <code>type <i>(string)</i></code>: The event type | ||
```ts | ||
const eventType = userCreatedEventType.type; | ||
// => "USER_CREATED" | ||
// => 'USER_CREATED' | ||
``` | ||
@@ -304,3 +301,3 @@ | ||
> ☝️ 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). | ||
> ☝️ Note that aggregates are always **computed on the fly**, and NOT stored. Changing them does not require any data migration whatsoever. | ||
@@ -314,2 +311,3 @@ ### 🎁 `EventStore` | ||
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 | ||
@@ -339,24 +337,192 @@ - Accessing events of an aggregate | ||
_...coming soon_ | ||
- <code>eventStoreId <i>(string)</i></code>: A string identifying the event store | ||
- <code>eventStoreEvents <i>(EventType[])</i></code>: The list of event types in the event store | ||
- <code>reduce <i>(EventType[])</i></code>: A [reducer function](#⚙️-reducer) that can be applied to the store event types | ||
- <code>storageAdapter <i>(?EventStorageAdapter)</i></code>: See [`EventStorageAdapter`](#💾-eventstorageadapter) | ||
<!-- > ☝️ Note that it's the ReturnType of the `reducer` function that is used to infer the `Aggregate` type of the EventStore. --> | ||
> ☝️ Note that it's the return type of the `reducer` that is used to infer the `Aggregate` type of the EventStore. It is important to type it explicitely. | ||
**Properties:** | ||
_...coming soon_ | ||
- <code>eventStoreId <i>(string)</i></code> | ||
**Type Helpers:** | ||
```ts | ||
const userEventStoreId = userEventStore.eventStoreId; | ||
// => 'USERS' | ||
``` | ||
_...coming soon_ | ||
- <code>eventStoreEvents <i>(EventType[])</i></code> | ||
<!-- EventStoreId | ||
```ts | ||
const userEventStoreEvents = userEventStore.eventStoreEvents; | ||
// => [userCreatedEventType, userRemovedEventType...] | ||
``` | ||
- <code>reduce <i>((Aggregate, EventType) => Aggregate)</i></code> | ||
```ts | ||
const reducer = userEventStore.reduce; | ||
// => usersReducer | ||
``` | ||
- <code>storageAdapter <i>?EventStorageAdapter</i></code>: See [`EventStorageAdapter`](#💾-eventstorageadapter) | ||
```ts | ||
const storageAdapter = userEventStore.storageAdapter; | ||
// => undefined (we did not provide one in this example) | ||
``` | ||
> ☝️ The `storageAdapter` is not read-only so you do not have to provide it right away. | ||
**Sync Methods:** | ||
- <code>getStorageAdapter <i>(() => EventStorageAdapter)</i></code>: Returns the event store event storage adapter if it exists. Throws an `UndefinedStorageAdapterError` if it doesn't. | ||
```ts | ||
import { UndefinedStorageAdapterError } from '@castore/core'; | ||
expect(() => userEventStore.getStorageAdapter()).toThrow( | ||
new UndefinedStorageAdapterError({ eventStoreId: 'USERS' }), | ||
); | ||
// => true | ||
``` | ||
- <code>buildAggregate <i>((eventDetails: EventDetail[], initialAggregate?: Aggregate) => Aggregate | undefined)</i></code>: Applies the event store reducer to a serie of events. | ||
```ts | ||
const johnDowAggregate = userEventStore.buildAggregate(johnDowEvents); | ||
``` | ||
**Async Methods:** | ||
The following methods interact with the data layer of your event store through its [`EventStorageAdapter`](#💾-eventstorageadapter). They will throw an `UndefinedStorageAdapterError` if you did not provide one. | ||
- <code>getEvents <i>((aggregateId: string, opt?: OptionsObj = {}) => Promise\<ResponseObj\>)</i></code>: Retrieves the events of an aggregate, ordered by `version`. Returns an empty array if no event is found for this `aggregateId`. | ||
`OptionsObj` contains the following attributes: | ||
- <code>minVersion <i>(?number)</i></code>: To retrieve events above a certain version | ||
- <code>maxVersion <i>(?number)</i></code>: To retrieve events below a certain version | ||
- <code>limit <i>(?number)</i></code>: Maximum number of events to retrieve | ||
- <code>reverse <i>(?boolean = false)</i></code>: To retrieve events in reverse order (does not require to swap `minVersion` and `maxVersion`) | ||
`ResponseObj` contains the following attributes: | ||
- <code>events <i>(EventDetail[])</i></code>: The aggregate events (possibly empty) | ||
```ts | ||
const { events: allEvents } = await userEventStore.getEvents(aggregateId); | ||
// => typed as UserEventDetail[] 🙌 | ||
// 👇 Retrieve a range of events | ||
const { events: rangedEvents } = await userEventStore.getEvents(aggregateId, { | ||
minVersion: 2, | ||
maxVersion: 5, | ||
}); | ||
// 👇 Retrieve the last event of the aggregate | ||
const { events: onlyLastEvent } = await userEventStore.getEvents(aggregateId, { | ||
reverse: true, | ||
limit: 1, | ||
}); | ||
``` | ||
- <code>getAggregate <i>((aggregateId: string, opt?: OptionsObj = {}) => Promise\<ResponseObj\>)</i></code>: Retrieves the events of an aggregate and build it. | ||
`OptionsObj` contains the following attributes: | ||
- <code>maxVersion <i>(?number)</i></code>: To retrieve aggregate below a certain version | ||
`ResponseObj` contains the following attributes: | ||
- <code>aggregate <i>(?Aggregate)</i></code>: The aggregate (possibly `undefined`) | ||
- <code>events <i>(EventDetail[])</i></code>: The aggregate events (possibly empty) | ||
- <code>lastEvent <i>(?EventDetail)</i></code>: The last event (possibly `undefined`) | ||
```ts | ||
const { aggregate: johnDow } = await userEventStore.getAggregate(aggregateId); | ||
// => typed as UserAggregate | undefined 🙌 | ||
// 👇 Retrieve an aggregate below a certain version | ||
const { aggregate: aggregateBelowVersion } = await userEventStore.getAggregate( | ||
aggregateId, | ||
{ maxVersion: 5 }, | ||
); | ||
// 👇 Returns the events if you need them | ||
const { aggregate, events } = await userEventStore.getAggregate(aggregateId); | ||
``` | ||
- <code>getExistingAggregate <i>((aggregateId: string, opt?: OptionsObj = {}) => Promise\<ResponseObj\>)</i></code>: Same as `getAggregate` method, but ensures that the aggregate exists. Throws an `AggregateNotFoundError` if no event is found for this `aggregateId`. | ||
```ts | ||
import { AggregateNotFoundError } from '@castore/core'; | ||
expect(async () => | ||
userEventStore.getExistingAggregate(unexistingId), | ||
).resolves.toThrow( | ||
new AggregateNotFoundError({ | ||
eventStoreId: 'USERS', | ||
aggregateId: unexistingId, | ||
}), | ||
); | ||
// true | ||
const { aggregate } = await userEventStore.getAggregate(aggregateId); | ||
// => 'aggregate' and 'lastEvent' are always defined 🙌 | ||
``` | ||
- <code>pushEvent <i>((eventDetail: EventDetail) => Promise\<void\>)</i></code>: Pushes a new event to the event store. Throws an `EventAlreadyExistsError` if an event already exists for the corresponding `aggregateId` and `version`. | ||
```ts | ||
await userEventStore.pushEvent({ | ||
aggregateId, | ||
version: lastVersion + 1, | ||
timestamp: new Date().toISOString(), | ||
type: 'USER_CREATED', // <= event type is correctly typed 🙌 | ||
payload, // <= payload is typed according to the provided event type 🙌 | ||
metadata, // <= same goes for metadata 🙌 | ||
}); | ||
``` | ||
- <code>listAggregateIds <i>((opt?: OptionsObj = {}) => Promise\<ResponseObj\>)</i></code>: Retrieves the list of `aggregateId` of an event store, ordered by `timestamp` of their first event. Returns an empty array if no aggregate is found. | ||
`OptionsObj` contains the following attributes: | ||
- <code>limit <i>(?number)</i></code>: Maximum number of aggregate ids to retrieve | ||
- <code>pageToken <i>(?string)</i></code>: To retrieve a paginated result of aggregate ids | ||
`ResponseObj` contains the following attributes: | ||
- <code>aggregateIds <i>(string[])</i></code>: The list of aggregate ids | ||
- <code>nextPageToken <i>(?string)</i></code>: A token for the next page of aggregate ids if one exists | ||
```ts | ||
const accAggregateIds: string = []; | ||
const { aggregateIds: firstPage, nextPageToken } = | ||
await userEventStore.getAggregate({ limit: 20 }); | ||
accAggregateIds.push(...firstPage); | ||
if (nextPageToken) { | ||
const { aggregateIds: secondPage } = await userEventStore.getAggregate({ | ||
limit: 20, | ||
pageToken: nextPageToken, | ||
}); | ||
accAggregateIds.push(...secondPage); | ||
} | ||
``` | ||
**Type Helpers:** | ||
- <code>EventStoreId</code>: Returns the `EventStore` id | ||
```ts | ||
import type { EventStoreId } from '@castore/core'; | ||
type UserEventStoreId = EventStoreId<typeof userEventStore>; | ||
// => "USERS" | ||
// => 'USERS' | ||
``` | ||
EventStoreEventsTypes | ||
- <code>EventStoreEventsTypes</code>: Returns the `EventStore` list of events types | ||
@@ -367,6 +533,6 @@ ```ts | ||
type UserEventsTypes = EventStoreEventsTypes<typeof userEventStore>; | ||
// => [typeof userCreatedEventTypeType, typeof userRemovedEventTypeType...] | ||
// => [typeof userCreatedEventType, typeof userRemovedEventType...] | ||
``` | ||
EventStoreEventsDetails | ||
- <code>EventStoreEventsDetails</code>: Returns the union of all the `EventStore` possible events details | ||
@@ -376,14 +542,26 @@ ```ts | ||
type UserEventsDetails = EventStoreEventsTypes<typeof userEventStore>; | ||
// => TODO | ||
type UserEventsDetails = EventStoreEventsDetails<typeof userEventStore>; | ||
// => EventTypeDetail<typeof userCreatedEventType> | ||
// | EventTypeDetail<typeof userRemovedEventType> | ||
// | ... | ||
``` | ||
EventStoreEventsDetails | ||
- <code>EventStoreReducer</code>: Returns the `EventStore` reducer | ||
```ts | ||
import type { EventStoreEventsDetails } from '@castore/core'; | ||
import type { EventStoreReducer } from '@castore/core'; | ||
type UserEventsDetails = EventStoreEventsTypes<typeof userEventStore>; | ||
``` --> | ||
type UserReducer = EventStoreReducer<typeof userEventStore>; | ||
// => Reducer<UserAggregate, UserEventsDetails> | ||
``` | ||
- <code>EventStoreAggregate</code>: Returns the `EventStore` aggregate | ||
```ts | ||
import type { EventStoreAggregate } from '@castore/core'; | ||
type UserReducer = EventStoreAggregate<typeof userEventStore>; | ||
// => UserAggregate | ||
``` | ||
### 💾 `EventStorageAdapter` | ||
@@ -408,8 +586,22 @@ | ||
_...coming soon_ | ||
Commands represent an intent to modify the state of your application. They usually result in pushing one or several events to your event stores. | ||
### 📸 `Snapshots` | ||
They typically consist in: | ||
_...coming soon_ | ||
- Fetching the required aggregates (if not the first event of a new aggregate) | ||
- Validating that the intent is acceptable in regards to the state of the application\* | ||
- Pushing new events with incremented versions | ||
> \* ☝️ Note that commands should NOT use read models for the validation step. Read models are not the source of truth, and may not contain the freshest state. | ||
Fetching and pushing events at separate non-simultaneously exposes your application to [race conditions](https://en.wikipedia.org/wiki/Race_condition). To counter that, commands executions are designed to be retried when an `EventAlreadyExistsError` is triggered. | ||
<!-- TODO, add schema --> | ||
_...technical description coming soon_ | ||
When writing on several event stores at once, it is important to use transactions, i.e. to make sure that all events are written or none. This ensures that the application is not in a corrupt state. | ||
Transactions accross event stores cannot be easily abstracted, so check you adapter library on how to achieve this. For instance, the [`DynamoDBEventStorageAdapter`](./packages/dynamodb-event-storage-adapter/README.md) exposes a [`pushEventsTransaction`](./packages/dynamodb-event-storage-adapter/src/utils/pushEventsTransaction.ts) util. | ||
## Resources | ||
@@ -419,11 +611,25 @@ | ||
_...coming soon_ | ||
Castore comes with a handy [Test Tool package](./packages/test-tools/README.md) that facilitates the writing of unit tests: It allows mocking event stores, populating them with an initial state and resetting them to it in a boilerplate-free and type-safe way. | ||
### 🔗 Packages List | ||
_...coming soon_ | ||
#### 🏷 Event Types | ||
- [JSON Schema Event Type](./packages/json-schema-event/README.md): DRY `EventType` definition using [JSON Schemas](http://json-schema.org/understanding-json-schema/reference/index.html) and [`json-schema-to-ts`](https://github.com/ThomasAribart/json-schema-to-ts) | ||
- [Zod Event Type](./packages/zod-event/README.md): DRY `EventType` definition using [`zod`](https://github.com/colinhacks/zod) | ||
#### 💾 Event Storage Adapters | ||
- [DynamoDB Event Storage Adapter](./packages/dynamodb-event-storage-adapter/README.md): Implementation of the `EventStorageAdapter` interface based on DynamoDB. | ||
- [In-Memory Event Storage Adapter](./packages/inmemory-event-storage-adapter/README.md): Implementation of the `EventStorageAdapter` interface using a local Node/JS object. To be used in manual or unit tests. | ||
#### 📨 Commands | ||
- [JSON Schema Command](./packages/json-schema-command/README.md): DRY `Command` definition using [JSON Schemas](http://json-schema.org/understanding-json-schema/reference/index.html) and [`json-schema-to-ts`](https://github.com/ThomasAribart/json-schema-to-ts) | ||
### 📖 Common Patterns | ||
_...coming soon_ | ||
- Simulating a future/past aggregate state: _...coming soon_ | ||
- Projecting on read models: _...coming soon_ | ||
- Replaying events: _...coming soon_ | ||
- Snapshotting: _...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
624
199286
103
1193