node-cqrs 0.16.4 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +2 -1
- package/dist/AbstractAggregate.js +178 -0
- package/dist/AbstractAggregate.js.map +1 -0
- package/dist/AbstractProjection.js +121 -0
- package/dist/AbstractProjection.js.map +1 -0
- package/dist/AbstractSaga.js +99 -0
- package/dist/AbstractSaga.js.map +1 -0
- package/dist/AggregateCommandHandler.js +85 -0
- package/dist/AggregateCommandHandler.js.map +1 -0
- package/dist/CommandBus.js +77 -0
- package/dist/CommandBus.js.map +1 -0
- package/dist/CqrsContainerBuilder.js +77 -0
- package/dist/CqrsContainerBuilder.js.map +1 -0
- package/dist/Event.js +43 -0
- package/dist/Event.js.map +1 -0
- package/dist/EventStore.js +229 -0
- package/dist/EventStore.js.map +1 -0
- package/dist/SagaEventHandler.js +117 -0
- package/dist/SagaEventHandler.js.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/infrastructure/InMemoryEventStorage.js +53 -0
- package/dist/infrastructure/InMemoryEventStorage.js.map +1 -0
- package/dist/infrastructure/InMemoryLock.js +68 -0
- package/dist/infrastructure/InMemoryLock.js.map +1 -0
- package/dist/infrastructure/InMemoryMessageBus.js +95 -0
- package/dist/infrastructure/InMemoryMessageBus.js.map +1 -0
- package/dist/infrastructure/InMemorySnapshotStorage.js +26 -0
- package/dist/infrastructure/InMemorySnapshotStorage.js.map +1 -0
- package/dist/infrastructure/InMemoryView.js +173 -0
- package/dist/infrastructure/InMemoryView.js.map +1 -0
- package/dist/infrastructure/utils/Deferred.js +38 -0
- package/dist/infrastructure/utils/Deferred.js.map +1 -0
- package/dist/infrastructure/utils/index.js +19 -0
- package/dist/infrastructure/utils/index.js.map +1 -0
- package/dist/infrastructure/utils/nextCycle.js +9 -0
- package/dist/infrastructure/utils/nextCycle.js.map +1 -0
- package/dist/interfaces.js +4 -0
- package/dist/interfaces.js.map +1 -0
- package/dist/utils/getClassName.js +10 -0
- package/dist/utils/getClassName.js.map +1 -0
- package/dist/utils/getHandledMessageTypes.js +18 -0
- package/dist/utils/getHandledMessageTypes.js.map +1 -0
- package/dist/utils/getHandler.js +20 -0
- package/dist/utils/getHandler.js.map +1 -0
- package/dist/utils/getMessageHandlerNames.js +38 -0
- package/dist/utils/getMessageHandlerNames.js.map +1 -0
- package/dist/utils/index.js +25 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/isClass.js +8 -0
- package/dist/utils/isClass.js.map +1 -0
- package/dist/utils/setupOneTimeEmitterSubscription.js +46 -0
- package/dist/utils/setupOneTimeEmitterSubscription.js.map +1 -0
- package/dist/utils/subscribe.js +39 -0
- package/dist/utils/subscribe.js.map +1 -0
- package/dist/utils/validateHandlers.js +21 -0
- package/dist/utils/validateHandlers.js.map +1 -0
- package/package.json +23 -12
- package/src/AbstractAggregate.ts +223 -0
- package/src/AbstractProjection.ts +172 -0
- package/src/AbstractSaga.ts +118 -0
- package/src/AggregateCommandHandler.ts +129 -0
- package/src/CommandBus.ts +98 -0
- package/src/CqrsContainerBuilder.ts +120 -0
- package/src/Event.ts +43 -0
- package/src/EventStore.ts +315 -0
- package/src/SagaEventHandler.ts +161 -0
- package/src/index.ts +26 -0
- package/src/infrastructure/InMemoryEventStorage.ts +68 -0
- package/src/infrastructure/InMemoryLock.ts +73 -0
- package/src/infrastructure/InMemoryMessageBus.ts +118 -0
- package/src/infrastructure/InMemorySnapshotStorage.ts +27 -0
- package/src/infrastructure/InMemoryView.ts +221 -0
- package/src/infrastructure/utils/Deferred.ts +41 -0
- package/src/infrastructure/utils/index.ts +2 -0
- package/src/infrastructure/utils/nextCycle.ts +4 -0
- package/src/interfaces.ts +328 -0
- package/src/utils/getClassName.ts +6 -0
- package/src/utils/{getHandledMessageTypes.js → getHandledMessageTypes.ts} +4 -8
- package/src/utils/{getHandler.js → getHandler.ts} +6 -7
- package/src/utils/{getMessageHandlerNames.js → getMessageHandlerNames.ts} +2 -9
- package/src/utils/index.ts +8 -0
- package/src/utils/{isClass.js → isClass.ts} +2 -4
- package/src/utils/setupOneTimeEmitterSubscription.ts +57 -0
- package/src/{subscribe.js → utils/subscribe.ts} +21 -18
- package/src/utils/{validateHandlers.js → validateHandlers.ts} +2 -8
- package/jsconfig.json +0 -15
- package/src/AbstractAggregate.js +0 -277
- package/src/AbstractProjection.js +0 -192
- package/src/AbstractSaga.js +0 -171
- package/src/AggregateCommandHandler.js +0 -126
- package/src/CommandBus.js +0 -91
- package/src/CqrsContainerBuilder.js +0 -131
- package/src/EventStore.js +0 -457
- package/src/EventStream.js +0 -63
- package/src/SagaEventHandler.js +0 -141
- package/src/index.js +0 -21
- package/src/infrastructure/InMemoryEventStorage.js +0 -76
- package/src/infrastructure/InMemoryMessageBus.js +0 -132
- package/src/infrastructure/InMemorySnapshotStorage.js +0 -40
- package/src/infrastructure/InMemoryView.js +0 -265
- package/src/utils/getClassName.js +0 -11
- package/src/utils/index.js +0 -6
- package/src/utils/nullLogger.js +0 -8
- package/types/index.d.ts +0 -16
- package/types/interfaces/IAggregate.d.ts +0 -30
- package/types/interfaces/IAggregateSnapshotStorage.d.ts +0 -4
- package/types/interfaces/ICommandBus.d.ts +0 -6
- package/types/interfaces/ICommandHandler.d.ts +0 -3
- package/types/interfaces/IConcurrentView.d.ts +0 -22
- package/types/interfaces/IEventReceptor.d.ts +0 -3
- package/types/interfaces/IEventStorage.d.ts +0 -20
- package/types/interfaces/IEventStore.d.ts +0 -18
- package/types/interfaces/IEventStream.d.ts +0 -13
- package/types/interfaces/ILogger.d.ts +0 -3
- package/types/interfaces/IMessageBus.d.ts +0 -5
- package/types/interfaces/IObserver.d.ts +0 -11
- package/types/interfaces/IProjection.d.ts +0 -10
- package/types/interfaces/ISaga.d.ts +0 -27
- package/types/interfaces/Identifier.d.ts +0 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setupOneTimeEmitterSubscription = setupOneTimeEmitterSubscription;
|
|
4
|
+
/**
|
|
5
|
+
* Create one-time eventEmitter subscription for one or multiple events that match a filter
|
|
6
|
+
*
|
|
7
|
+
* @param {IObservable} emitter
|
|
8
|
+
* @param {string[]} messageTypes Array of event type to subscribe to
|
|
9
|
+
* @param {function(IEvent):any} [handler] Optional handler to execute for a first event received
|
|
10
|
+
* @param {function(IEvent):boolean} [filter] Optional filter to apply before executing a handler
|
|
11
|
+
* @param {ILogger} logger
|
|
12
|
+
* @return {Promise<IEvent>} Resolves to first event that passes filter
|
|
13
|
+
*/
|
|
14
|
+
function setupOneTimeEmitterSubscription(emitter, messageTypes, filter, handler, logger) {
|
|
15
|
+
if (typeof emitter !== 'object' || !emitter)
|
|
16
|
+
throw new TypeError('emitter argument must be an Object');
|
|
17
|
+
if (!Array.isArray(messageTypes) || messageTypes.some(m => !m || typeof m !== 'string'))
|
|
18
|
+
throw new TypeError('messageTypes argument must be an Array of non-empty Strings');
|
|
19
|
+
if (handler && typeof handler !== 'function')
|
|
20
|
+
throw new TypeError('handler argument, when specified, must be a Function');
|
|
21
|
+
if (filter && typeof filter !== 'function')
|
|
22
|
+
throw new TypeError('filter argument, when specified, must be a Function');
|
|
23
|
+
return new Promise(resolve => {
|
|
24
|
+
// handler will be invoked only once,
|
|
25
|
+
// even if multiple events have been emitted before subscription was destroyed
|
|
26
|
+
// https://nodejs.org/api/events.html#events_emitter_removelistener_eventname_listener
|
|
27
|
+
let handled = false;
|
|
28
|
+
function filteredHandler(event) {
|
|
29
|
+
if (filter && !filter(event))
|
|
30
|
+
return;
|
|
31
|
+
if (handled)
|
|
32
|
+
return;
|
|
33
|
+
handled = true;
|
|
34
|
+
for (const messageType of messageTypes)
|
|
35
|
+
emitter.off(messageType, filteredHandler);
|
|
36
|
+
logger?.debug(`'${event.type}' received, one-time subscription to '${messageTypes.join(',')}' removed`);
|
|
37
|
+
if (handler)
|
|
38
|
+
handler(event);
|
|
39
|
+
resolve(event);
|
|
40
|
+
}
|
|
41
|
+
for (const messageType of messageTypes)
|
|
42
|
+
emitter.on(messageType, filteredHandler);
|
|
43
|
+
logger?.debug(`set up one-time ${filter ? 'filtered subscription' : 'subscription'} to '${messageTypes.join(',')}'`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=setupOneTimeEmitterSubscription.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setupOneTimeEmitterSubscription.js","sourceRoot":"","sources":["../../src/utils/setupOneTimeEmitterSubscription.ts"],"names":[],"mappings":";;AAYA,0EA4CC;AAtDD;;;;;;;;;GASG;AACH,SAAgB,+BAA+B,CAC9C,OAAoB,EACpB,YAAsB,EACtB,MAA+B,EAC/B,OAA6B,EAC7B,MAAgB;IAEhB,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,OAAO;QAC1C,MAAM,IAAI,SAAS,CAAC,oCAAoC,CAAC,CAAC;IAC3D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAC;QACtF,MAAM,IAAI,SAAS,CAAC,6DAA6D,CAAC,CAAC;IACpF,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,UAAU;QAC3C,MAAM,IAAI,SAAS,CAAC,sDAAsD,CAAC,CAAC;IAC7E,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,UAAU;QACzC,MAAM,IAAI,SAAS,CAAC,qDAAqD,CAAC,CAAC;IAE5E,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;QAE5B,qCAAqC;QACrC,8EAA8E;QAC9E,sFAAsF;QACtF,IAAI,OAAO,GAAG,KAAK,CAAC;QAEpB,SAAS,eAAe,CAAC,KAAa;YACrC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;gBAAE,OAAO;YACrC,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YAEf,KAAK,MAAM,WAAW,IAAI,YAAY;gBACrC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;YAE3C,MAAM,EAAE,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,yCAAyC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;YAExG,IAAI,OAAO;gBACV,OAAO,CAAC,KAAK,CAAC,CAAC;YAEhB,OAAO,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC;QAED,KAAK,MAAM,WAAW,IAAI,YAAY;YACrC,OAAO,CAAC,EAAE,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;QAE1C,MAAM,EAAE,KAAK,CAAC,mBAAmB,MAAM,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,cAAc,QAAQ,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtH,CAAC,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.subscribe = subscribe;
|
|
4
|
+
const getHandler_1 = require("./getHandler");
|
|
5
|
+
const getHandledMessageTypes_1 = require("./getHandledMessageTypes");
|
|
6
|
+
const unique = (arr) => [...new Set(arr)];
|
|
7
|
+
/**
|
|
8
|
+
* Subscribe observer to observable
|
|
9
|
+
*/
|
|
10
|
+
function subscribe(observable, observer, options = {}) {
|
|
11
|
+
if (typeof observable !== 'object' || !observable)
|
|
12
|
+
throw new TypeError('observable argument must be an Object');
|
|
13
|
+
if (typeof observable.on !== 'function')
|
|
14
|
+
throw new TypeError('observable.on must be a Function');
|
|
15
|
+
if (typeof observer !== 'object' || !observer)
|
|
16
|
+
throw new TypeError('observer argument must be an Object');
|
|
17
|
+
const { masterHandler, messageTypes, queueName } = options;
|
|
18
|
+
if (masterHandler && typeof masterHandler !== 'function')
|
|
19
|
+
throw new TypeError('masterHandler parameter, when provided, must be a Function');
|
|
20
|
+
if (queueName && typeof observable.queue !== 'function')
|
|
21
|
+
throw new TypeError('observable.queue, when queueName is specified, must be a Function');
|
|
22
|
+
const subscribeTo = messageTypes || (0, getHandledMessageTypes_1.getHandledMessageTypes)(observer);
|
|
23
|
+
if (!Array.isArray(subscribeTo))
|
|
24
|
+
throw new TypeError('either options.messageTypes, observer.handles or ObserverType.handles is required');
|
|
25
|
+
for (const messageType of unique(subscribeTo)) {
|
|
26
|
+
const handler = masterHandler || (0, getHandler_1.getHandler)(observer, messageType);
|
|
27
|
+
if (!handler)
|
|
28
|
+
throw new Error(`'${messageType}' handler is not defined or not a function`);
|
|
29
|
+
if (queueName) {
|
|
30
|
+
if (!observable.queue)
|
|
31
|
+
throw new TypeError('Observer does not support named queues');
|
|
32
|
+
observable.queue(queueName).on(messageType, handler);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
observable.on(messageType, handler);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=subscribe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../src/utils/subscribe.ts"],"names":[],"mappings":";;AASA,8BAyCC;AAjDD,6CAA0C;AAC1C,qEAAkE;AAElE,MAAM,MAAM,GAAG,CAAI,GAAQ,EAAO,EAAE,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAEvD;;GAEG;AACH,SAAgB,SAAS,CACxB,UAAuB,EACvB,QAAgB,EAChB,UAII,EAAE;IAEN,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,UAAU;QAChD,MAAM,IAAI,SAAS,CAAC,uCAAuC,CAAC,CAAC;IAC9D,IAAI,OAAO,UAAU,CAAC,EAAE,KAAK,UAAU;QACtC,MAAM,IAAI,SAAS,CAAC,kCAAkC,CAAC,CAAC;IACzD,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ;QAC5C,MAAM,IAAI,SAAS,CAAC,qCAAqC,CAAC,CAAC;IAE5D,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAC3D,IAAI,aAAa,IAAI,OAAO,aAAa,KAAK,UAAU;QACvD,MAAM,IAAI,SAAS,CAAC,4DAA4D,CAAC,CAAC;IACnF,IAAI,SAAS,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,UAAU;QACtD,MAAM,IAAI,SAAS,CAAC,mEAAmE,CAAC,CAAC;IAE1F,MAAM,WAAW,GAAG,YAAY,IAAI,IAAA,+CAAsB,EAAC,QAAQ,CAAC,CAAC;IACrE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC;QAC9B,MAAM,IAAI,SAAS,CAAC,mFAAmF,CAAC,CAAC;IAE1G,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/C,MAAM,OAAO,GAAG,aAAa,IAAI,IAAA,uBAAU,EAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAClE,IAAI,CAAC,OAAO;YACZ,MAAM,IAAI,KAAK,CAAC,IAAI,WAAW,4CAA4C,CAAC,CAAC;QAE9E,IAAI,SAAS,EAAE,CAAC;YACf,IAAG,CAAC,UAAU,CAAC,KAAK;gBACnB,MAAM,IAAI,SAAS,CAAC,wCAAwC,CAAC,CAAC;YAE/D,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACtD,CAAC;aACI,CAAC;YACL,UAAU,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACrC,CAAC;IACF,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateHandlers = validateHandlers;
|
|
4
|
+
const getHandler_1 = require("./getHandler");
|
|
5
|
+
/**
|
|
6
|
+
* Ensure instance has handlers declared for all handled message types
|
|
7
|
+
*/
|
|
8
|
+
function validateHandlers(instance, handlesFieldName = 'handles') {
|
|
9
|
+
if (!instance)
|
|
10
|
+
throw new TypeError('instance argument required');
|
|
11
|
+
const messageTypes = Object.getPrototypeOf(instance).constructor[handlesFieldName];
|
|
12
|
+
if (messageTypes === undefined)
|
|
13
|
+
return;
|
|
14
|
+
if (!Array.isArray(messageTypes))
|
|
15
|
+
throw new TypeError('handles getter, when defined, must return an Array of Strings');
|
|
16
|
+
for (const type of messageTypes) {
|
|
17
|
+
if (!(0, getHandler_1.getHandler)(instance, type))
|
|
18
|
+
throw new Error(`'${type}' handler is not defined or not a function`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=validateHandlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateHandlers.js","sourceRoot":"","sources":["../../src/utils/validateHandlers.ts"],"names":[],"mappings":";;AAKA,4CAaC;AAlBD,6CAA0C;AAE1C;;GAEG;AACH,SAAgB,gBAAgB,CAAC,QAAgB,EAAE,gBAAgB,GAAG,SAAS;IAC9E,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAC;IAEjE,MAAM,YAAY,GAAG,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC,gBAAgB,CAAC,CAAC;IACnF,IAAI,YAAY,KAAK,SAAS;QAC7B,OAAO;IACR,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC;QAC/B,MAAM,IAAI,SAAS,CAAC,+DAA+D,CAAC,CAAC;IAEtF,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QACjC,IAAI,CAAC,IAAA,uBAAU,EAAC,QAAQ,EAAE,IAAI,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,4CAA4C,CAAC,CAAC;IACxE,CAAC;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-cqrs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Basic ES6 backbone for CQRS app development",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,20 +18,25 @@
|
|
|
18
18
|
"domain",
|
|
19
19
|
"eventstore"
|
|
20
20
|
],
|
|
21
|
-
"main": "
|
|
22
|
-
"types": "
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./src/index.ts",
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=10.3.0"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"
|
|
28
|
-
"test": "
|
|
29
|
-
"test:coverage": "
|
|
27
|
+
"pretest": "npm run build",
|
|
28
|
+
"test": "jest --verbose tests/unit",
|
|
29
|
+
"test:coverage": "jest --collect-coverage tests/unit",
|
|
30
|
+
"pretest:integration": "npm run build",
|
|
31
|
+
"test:integration": "jest --verbose examples/user-domain-tests",
|
|
30
32
|
"pretest:coveralls": "npm run test:coverage",
|
|
31
33
|
"test:coveralls": "cat ./coverage/lcov.info | coveralls",
|
|
32
34
|
"posttest:coveralls": "rm -rf ./coverage",
|
|
33
35
|
"changelog": "conventional-changelog -n ./scripts/changelog -i CHANGELOG.md -s",
|
|
34
|
-
"
|
|
36
|
+
"clean": "tsc --build --clean",
|
|
37
|
+
"build": "tsc --build",
|
|
38
|
+
"prepare": "npm run build",
|
|
39
|
+
"preversion": "npm test",
|
|
35
40
|
"version": "npm run changelog && git add CHANGELOG.md"
|
|
36
41
|
},
|
|
37
42
|
"author": "@snatalenko",
|
|
@@ -41,11 +46,17 @@
|
|
|
41
46
|
"di0": "^1.0.0"
|
|
42
47
|
},
|
|
43
48
|
"devDependencies": {
|
|
44
|
-
"chai": "^4.3.
|
|
49
|
+
"@types/chai": "^4.3.17",
|
|
50
|
+
"@types/jest": "^29.5.12",
|
|
51
|
+
"@types/node": "^20.14.14",
|
|
52
|
+
"@types/sinon": "^10.0.20",
|
|
53
|
+
"chai": "^4.5.0",
|
|
54
|
+
"conventional-changelog": "^3.1.25",
|
|
45
55
|
"coveralls": "^3.1.1",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
56
|
+
"jest": "^29.7.0",
|
|
57
|
+
"sinon": "^15.2.0",
|
|
58
|
+
"ts-jest": "^29.2.4",
|
|
59
|
+
"ts-node": "^10.9.2",
|
|
60
|
+
"typescript": "^5.5.4"
|
|
50
61
|
}
|
|
51
62
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IAggregate,
|
|
3
|
+
IMutableAggregateState,
|
|
4
|
+
ICommand,
|
|
5
|
+
Identifier,
|
|
6
|
+
IEvent,
|
|
7
|
+
IEventSet,
|
|
8
|
+
IAggregateConstructorParams
|
|
9
|
+
} from "./interfaces";
|
|
10
|
+
|
|
11
|
+
import { getClassName, validateHandlers, getHandler } from './utils';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Deep-clone simple JS object
|
|
15
|
+
*/
|
|
16
|
+
function clone<T>(obj: T): T {
|
|
17
|
+
return JSON.parse(JSON.stringify(obj));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SNAPSHOT_EVENT_TYPE = 'snapshot';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Base class for Aggregate definition
|
|
24
|
+
*/
|
|
25
|
+
export abstract class AbstractAggregate<TState extends IMutableAggregateState | object | void> implements IAggregate {
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional list of commands handled by Aggregate.
|
|
29
|
+
*
|
|
30
|
+
* If not overridden in Aggregate implementation,
|
|
31
|
+
* `AggregateCommandHandler` will treat all public methods as command handlers
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* return ['createUser', 'changePassword'];
|
|
35
|
+
*/
|
|
36
|
+
static get handles(): string[] | undefined {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#id: Identifier;
|
|
41
|
+
#changes: IEvent[] = [];
|
|
42
|
+
#version: number = 0;
|
|
43
|
+
#snapshotVersion: number | undefined;
|
|
44
|
+
|
|
45
|
+
/** Internal aggregate state */
|
|
46
|
+
protected state: TState;
|
|
47
|
+
|
|
48
|
+
/** Command being handled by aggregate */
|
|
49
|
+
protected command?: ICommand;
|
|
50
|
+
|
|
51
|
+
/** Unique aggregate instance identifier */
|
|
52
|
+
get id(): Identifier {
|
|
53
|
+
return this.#id;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Aggregate instance version */
|
|
57
|
+
get version(): number {
|
|
58
|
+
return this.#version;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Restored snapshot version */
|
|
62
|
+
get snapshotVersion(): number | undefined {
|
|
63
|
+
return this.#snapshotVersion;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Events emitted by Aggregate */
|
|
67
|
+
get changes(): IEventSet {
|
|
68
|
+
return [...this.#changes];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Override to define whether an aggregate state snapshot should be taken
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // create snapshot every 50 events
|
|
76
|
+
* return this.version % 50 === 0;
|
|
77
|
+
*/
|
|
78
|
+
get shouldTakeSnapshot(): boolean {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
constructor(options: IAggregateConstructorParams<TState>) {
|
|
83
|
+
const { id, state, events } = options;
|
|
84
|
+
if (!id)
|
|
85
|
+
throw new TypeError('id argument required');
|
|
86
|
+
if (state && typeof state !== 'object')
|
|
87
|
+
throw new TypeError('state argument, when provided, must be an Object');
|
|
88
|
+
if (events && !Array.isArray(events))
|
|
89
|
+
throw new TypeError('events argument, when provided, must be an Array');
|
|
90
|
+
|
|
91
|
+
this.#id = id;
|
|
92
|
+
|
|
93
|
+
validateHandlers(this);
|
|
94
|
+
|
|
95
|
+
if (state)
|
|
96
|
+
this.state = state;
|
|
97
|
+
|
|
98
|
+
if (events)
|
|
99
|
+
events.forEach(event => this.mutate(event));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Pass command to command handler */
|
|
103
|
+
handle(command: ICommand) {
|
|
104
|
+
if (!command)
|
|
105
|
+
throw new TypeError('command argument required');
|
|
106
|
+
if (!command.type)
|
|
107
|
+
throw new TypeError('command.type argument required');
|
|
108
|
+
|
|
109
|
+
const handler = getHandler(this, command.type);
|
|
110
|
+
if (!handler)
|
|
111
|
+
throw new Error(`'${command.type}' handler is not defined or not a function`);
|
|
112
|
+
|
|
113
|
+
this.command = command;
|
|
114
|
+
|
|
115
|
+
return handler.call(this, command.payload, command.context);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Mutate aggregate state and increment aggregate version */
|
|
119
|
+
mutate(event) {
|
|
120
|
+
if (event.aggregateVersion !== undefined)
|
|
121
|
+
this.#version = event.aggregateVersion;
|
|
122
|
+
|
|
123
|
+
if (event.type === SNAPSHOT_EVENT_TYPE) {
|
|
124
|
+
this.#snapshotVersion = event.aggregateVersion;
|
|
125
|
+
this.restoreSnapshot(event);
|
|
126
|
+
}
|
|
127
|
+
else if (this.state) {
|
|
128
|
+
const handler = 'mutate' in this.state ?
|
|
129
|
+
this.state.mutate :
|
|
130
|
+
getHandler(this.state, event.type);
|
|
131
|
+
if (handler)
|
|
132
|
+
handler.call(this.state, event);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.#version += 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Format and register aggregate event and mutate aggregate state */
|
|
139
|
+
protected emit<TPayload>(type: string, payload?: TPayload) {
|
|
140
|
+
if (typeof type !== 'string' || !type.length)
|
|
141
|
+
throw new TypeError('type argument must be a non-empty string');
|
|
142
|
+
|
|
143
|
+
const event = this.makeEvent<TPayload>(type, payload, this.command);
|
|
144
|
+
|
|
145
|
+
this.emitRaw(event);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Format event based on a current aggregate state and a command being executed */
|
|
149
|
+
protected makeEvent<TPayload>(type: string, payload?: TPayload, sourceCommand?: ICommand): IEvent<TPayload> {
|
|
150
|
+
const event: IEvent<TPayload> = {
|
|
151
|
+
aggregateId: this.id,
|
|
152
|
+
aggregateVersion: this.version,
|
|
153
|
+
type,
|
|
154
|
+
payload
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (sourceCommand) {
|
|
158
|
+
// augment event with command context
|
|
159
|
+
const { context, sagaId, sagaVersion } = sourceCommand;
|
|
160
|
+
if (context !== undefined)
|
|
161
|
+
event.context = context;
|
|
162
|
+
if (sagaId !== undefined)
|
|
163
|
+
event.sagaId = sagaId;
|
|
164
|
+
if (sagaVersion !== undefined)
|
|
165
|
+
event.sagaVersion = sagaVersion;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return event;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Register aggregate event and mutate aggregate state */
|
|
172
|
+
protected emitRaw<TPayload>(event: IEvent<TPayload>): void {
|
|
173
|
+
if (!event)
|
|
174
|
+
throw new TypeError('event argument required');
|
|
175
|
+
if (!event.aggregateId)
|
|
176
|
+
throw new TypeError('event.aggregateId argument required');
|
|
177
|
+
if (typeof event.aggregateVersion !== 'number')
|
|
178
|
+
throw new TypeError('event.aggregateVersion argument must be a Number');
|
|
179
|
+
if (typeof event.type !== 'string' || !event.type.length)
|
|
180
|
+
throw new TypeError('event.type argument must be a non-empty String');
|
|
181
|
+
|
|
182
|
+
this.mutate(event);
|
|
183
|
+
|
|
184
|
+
this.#changes.push(event);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Take an aggregate state snapshot and add it to the changes queue
|
|
189
|
+
*/
|
|
190
|
+
takeSnapshot() {
|
|
191
|
+
this.emit(SNAPSHOT_EVENT_TYPE, this.makeSnapshot());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Create an aggregate state snapshot */
|
|
195
|
+
makeSnapshot(): TState {
|
|
196
|
+
if (!this.state)
|
|
197
|
+
throw new Error('state property is empty, either define state or override makeSnapshot method');
|
|
198
|
+
|
|
199
|
+
return clone(this.state);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Restore aggregate state from a snapshot */
|
|
203
|
+
protected restoreSnapshot(snapshotEvent: IEvent<TState>) {
|
|
204
|
+
if (!snapshotEvent)
|
|
205
|
+
throw new TypeError('snapshotEvent argument required');
|
|
206
|
+
if (!snapshotEvent.type)
|
|
207
|
+
throw new TypeError('snapshotEvent.type argument required');
|
|
208
|
+
if (!snapshotEvent.payload)
|
|
209
|
+
throw new TypeError('snapshotEvent.payload argument required');
|
|
210
|
+
|
|
211
|
+
if (snapshotEvent.type !== SNAPSHOT_EVENT_TYPE)
|
|
212
|
+
throw new Error(`${SNAPSHOT_EVENT_TYPE} event type expected`);
|
|
213
|
+
if (!this.state)
|
|
214
|
+
throw new Error('state property is empty, either defined state or override restoreSnapshot method');
|
|
215
|
+
|
|
216
|
+
Object.assign(this.state, clone(snapshotEvent.payload));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Get human-readable aggregate identifier */
|
|
220
|
+
toString(): string {
|
|
221
|
+
return `${getClassName(this)} ${this.id} (v${this.version})`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { InMemoryView } from './infrastructure/InMemoryView';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
IProjectionView,
|
|
5
|
+
IEvent,
|
|
6
|
+
IPersistentView,
|
|
7
|
+
IEventStore,
|
|
8
|
+
IExtendableLogger,
|
|
9
|
+
ILogger,
|
|
10
|
+
IProjection,
|
|
11
|
+
IViewFactory
|
|
12
|
+
} from "./interfaces";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
getClassName,
|
|
16
|
+
validateHandlers,
|
|
17
|
+
getHandler,
|
|
18
|
+
getHandledMessageTypes,
|
|
19
|
+
subscribe
|
|
20
|
+
} from './utils';
|
|
21
|
+
|
|
22
|
+
const isProjectionView = (view: IProjectionView): view is IProjectionView =>
|
|
23
|
+
'ready' in view &&
|
|
24
|
+
'lock' in view &&
|
|
25
|
+
'unlock' in view &&
|
|
26
|
+
'once' in view;
|
|
27
|
+
|
|
28
|
+
const asProjectionView = (view: any): IProjectionView | undefined =>
|
|
29
|
+
(isProjectionView(view) ? view : undefined);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Base class for Projection definition
|
|
33
|
+
*/
|
|
34
|
+
export abstract class AbstractProjection<TView extends IProjectionView | IPersistentView> implements IProjection<TView> {
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Optional list of event types being handled by projection.
|
|
38
|
+
* Can be overridden in projection implementation.
|
|
39
|
+
* If not overridden, will detect event types from event handlers declared on the Projection class
|
|
40
|
+
*/
|
|
41
|
+
static get handles(): string[] | undefined {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default view associated with projection
|
|
47
|
+
*/
|
|
48
|
+
get view(): TView {
|
|
49
|
+
if (!this.#view)
|
|
50
|
+
this.#view = this.#viewFactory();
|
|
51
|
+
|
|
52
|
+
return this.#view;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#viewFactory: IViewFactory<TView>;
|
|
56
|
+
#view?: TView;
|
|
57
|
+
|
|
58
|
+
protected _logger?: ILogger;
|
|
59
|
+
|
|
60
|
+
get collectionName(): string {
|
|
61
|
+
return getClassName(this);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Indicates if view should be restored from EventStore on start.
|
|
66
|
+
* Override for custom behavior.
|
|
67
|
+
*/
|
|
68
|
+
get shouldRestoreView(): boolean | Promise<boolean> {
|
|
69
|
+
return (this.view instanceof Map)
|
|
70
|
+
|| (this.view instanceof InMemoryView);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
constructor({
|
|
74
|
+
view,
|
|
75
|
+
viewFactory = InMemoryView.factory,
|
|
76
|
+
logger
|
|
77
|
+
}: {
|
|
78
|
+
view?: TView,
|
|
79
|
+
viewFactory?: IViewFactory<TView>,
|
|
80
|
+
logger?: ILogger | IExtendableLogger
|
|
81
|
+
} = {}) {
|
|
82
|
+
validateHandlers(this);
|
|
83
|
+
|
|
84
|
+
this.#viewFactory = view ?
|
|
85
|
+
() => view :
|
|
86
|
+
viewFactory;
|
|
87
|
+
|
|
88
|
+
this._logger = logger && 'child' in logger ?
|
|
89
|
+
logger.child({ service: getClassName(this) }) :
|
|
90
|
+
logger;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Subscribe to event store */
|
|
94
|
+
async subscribe(eventStore: IEventStore): Promise<void> {
|
|
95
|
+
subscribe(eventStore, this, {
|
|
96
|
+
masterHandler: (e: IEvent) => this.project(e)
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await this.restore(eventStore);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Pass event to projection event handler */
|
|
103
|
+
async project(event: IEvent): Promise<void> {
|
|
104
|
+
const concurrentView = asProjectionView(this.view);
|
|
105
|
+
if (concurrentView && !concurrentView.ready)
|
|
106
|
+
await concurrentView.once('ready');
|
|
107
|
+
|
|
108
|
+
return this._project(event);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Pass event to projection event handler, without awaiting for restore operation to complete */
|
|
112
|
+
protected async _project(event: IEvent): Promise<void> {
|
|
113
|
+
const handler = getHandler(this, event.type);
|
|
114
|
+
if (!handler)
|
|
115
|
+
throw new Error(`'${event.type}' handler is not defined or not a function`);
|
|
116
|
+
|
|
117
|
+
return handler.call(this, event);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Restore projection view from event store */
|
|
121
|
+
async restore(eventStore: IEventStore): Promise<void> {
|
|
122
|
+
// lock the view to ensure same restoring procedure
|
|
123
|
+
// won't be performed by another projection instance
|
|
124
|
+
const concurrentView = asProjectionView(this.view);
|
|
125
|
+
if (concurrentView)
|
|
126
|
+
await concurrentView.lock();
|
|
127
|
+
|
|
128
|
+
const shouldRestore = await this.shouldRestoreView;
|
|
129
|
+
if (shouldRestore)
|
|
130
|
+
await this._restore(eventStore);
|
|
131
|
+
|
|
132
|
+
if (concurrentView)
|
|
133
|
+
concurrentView.unlock();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Restore projection view from event store */
|
|
137
|
+
protected async _restore(eventStore: IEventStore): Promise<void> {
|
|
138
|
+
if (!eventStore)
|
|
139
|
+
throw new TypeError('eventStore argument required');
|
|
140
|
+
if (typeof eventStore.getAllEvents !== 'function')
|
|
141
|
+
throw new TypeError('eventStore.getAllEvents must be a Function');
|
|
142
|
+
|
|
143
|
+
this._logger?.debug('retrieving events and restoring projection...');
|
|
144
|
+
|
|
145
|
+
const messageTypes = getHandledMessageTypes(this);
|
|
146
|
+
const eventsIterable = eventStore.getAllEvents(messageTypes);
|
|
147
|
+
let eventsCount = 0;
|
|
148
|
+
const startTs = Date.now();
|
|
149
|
+
|
|
150
|
+
for await (const event of eventsIterable) {
|
|
151
|
+
try {
|
|
152
|
+
await this._project(event);
|
|
153
|
+
eventsCount += 1;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
this._onRestoringError(err, event);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this._logger?.info(`view restored (${this.view}) from ${eventsCount} event(s) in ${Date.now() - startTs} ms`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Handle error on restoring. Logs and throws error by default */
|
|
164
|
+
protected _onRestoringError(error: Error, event: IEvent) {
|
|
165
|
+
this._logger?.error(`view restoring has failed (view will remain locked): ${error.message}`, {
|
|
166
|
+
service: getClassName(this),
|
|
167
|
+
event,
|
|
168
|
+
stack: error.stack
|
|
169
|
+
});
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|