@flusys/nestjs-shared 6.4.1 → 7.0.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/README.md +437 -0
- package/cjs/classes/api-service.class.js +71 -8
- package/cjs/classes/api-service.class.spec.js +220 -0
- package/cjs/classes/disabled-event-bus.class.js +15 -0
- package/cjs/classes/disabled-event-bus.class.spec.js +40 -0
- package/cjs/classes/domain-event.publisher.js +76 -0
- package/cjs/classes/domain-event.publisher.spec.js +234 -0
- package/cjs/classes/event-bus.registry.js +69 -0
- package/cjs/classes/event-bus.registry.spec.js +116 -0
- package/cjs/classes/event-transports/event-transport.registry.js +41 -0
- package/cjs/classes/event-transports/event-transport.registry.spec.js +31 -0
- package/cjs/classes/event-transports/index.js +23 -0
- package/cjs/classes/event-transports/kafka-transport.optional.js +151 -0
- package/cjs/classes/event-transports/kafka-transport.optional.spec.js +199 -0
- package/cjs/classes/event-transports/memory-transport.js +54 -0
- package/cjs/classes/event-transports/memory-transport.spec.js +58 -0
- package/cjs/classes/event-transports/rabbitmq-transport.optional.js +184 -0
- package/cjs/classes/event-transports/rabbitmq-transport.optional.spec.js +256 -0
- package/cjs/classes/hybrid-event-bus.class.js +261 -0
- package/cjs/classes/hybrid-event-bus.class.spec.js +189 -0
- package/cjs/classes/index.js +5 -0
- package/cjs/constants/index.js +24 -0
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/on-domain-event.decorator.js +13 -0
- package/cjs/decorators/on-domain-event.decorator.spec.js +50 -0
- package/cjs/entities/user-root.js +2 -2
- package/cjs/enums/event-action.enum.js +18 -0
- package/cjs/enums/index.js +1 -0
- package/cjs/interfaces/event-bus.interface.js +4 -0
- package/cjs/interfaces/index.js +1 -0
- package/cjs/modules/event-bus/event-bus.explorer.js +124 -0
- package/cjs/modules/event-bus/event-bus.explorer.spec.js +189 -0
- package/cjs/modules/event-bus/event-bus.integration.spec.js +254 -0
- package/cjs/modules/event-bus/event-bus.module.js +94 -0
- package/cjs/modules/event-bus/event-bus.module.spec.js +149 -0
- package/cjs/modules/event-bus/module-events.provider.js +39 -0
- package/cjs/modules/event-bus/module-events.provider.spec.js +56 -0
- package/cjs/modules/index.js +3 -0
- package/cjs/utils/event-pattern.util.js +98 -0
- package/cjs/utils/event-pattern.util.spec.js +234 -0
- package/cjs/utils/index.js +2 -0
- package/cjs/utils/optional-module.util.js +35 -0
- package/cjs/utils/optional-module.util.spec.js +14 -0
- package/classes/api-service.class.d.ts +11 -1
- package/classes/disabled-event-bus.class.d.ts +6 -0
- package/classes/domain-event.publisher.d.ts +14 -0
- package/classes/event-bus.registry.d.ts +14 -0
- package/classes/event-transports/event-transport.registry.d.ts +10 -0
- package/classes/event-transports/index.d.ts +2 -0
- package/classes/event-transports/kafka-transport.optional.d.ts +21 -0
- package/classes/event-transports/memory-transport.d.ts +13 -0
- package/classes/event-transports/rabbitmq-transport.optional.d.ts +27 -0
- package/classes/hybrid-event-bus.class.d.ts +32 -0
- package/classes/index.d.ts +5 -0
- package/constants/index.d.ts +6 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/on-domain-event.decorator.d.ts +1 -0
- package/enums/event-action.enum.d.ts +7 -0
- package/enums/index.d.ts +1 -0
- package/fesm/classes/api-service.class.js +71 -8
- package/fesm/classes/api-service.class.spec.js +220 -0
- package/fesm/classes/disabled-event-bus.class.js +11 -0
- package/fesm/classes/disabled-event-bus.class.spec.js +36 -0
- package/fesm/classes/domain-event.publisher.js +63 -0
- package/fesm/classes/domain-event.publisher.spec.js +230 -0
- package/fesm/classes/event-bus.registry.js +65 -0
- package/fesm/classes/event-bus.registry.spec.js +112 -0
- package/fesm/classes/event-transports/event-transport.registry.js +35 -0
- package/fesm/classes/event-transports/event-transport.registry.spec.js +27 -0
- package/fesm/classes/event-transports/index.js +6 -0
- package/fesm/classes/event-transports/kafka-transport.optional.js +144 -0
- package/fesm/classes/event-transports/kafka-transport.optional.spec.js +195 -0
- package/fesm/classes/event-transports/memory-transport.js +46 -0
- package/fesm/classes/event-transports/memory-transport.spec.js +54 -0
- package/fesm/classes/event-transports/rabbitmq-transport.optional.js +174 -0
- package/fesm/classes/event-transports/rabbitmq-transport.optional.spec.js +252 -0
- package/fesm/classes/hybrid-event-bus.class.js +217 -0
- package/fesm/classes/hybrid-event-bus.class.spec.js +185 -0
- package/fesm/classes/index.js +5 -0
- package/fesm/constants/index.js +7 -0
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/on-domain-event.decorator.js +10 -0
- package/fesm/decorators/on-domain-event.decorator.spec.js +46 -0
- package/fesm/entities/user-root.js +1 -1
- package/fesm/enums/event-action.enum.js +8 -0
- package/fesm/enums/index.js +1 -0
- package/fesm/interfaces/event-bus.interface.js +1 -0
- package/fesm/interfaces/index.js +1 -0
- package/fesm/modules/event-bus/event-bus.explorer.js +114 -0
- package/fesm/modules/event-bus/event-bus.explorer.spec.js +185 -0
- package/fesm/modules/event-bus/event-bus.integration.spec.js +250 -0
- package/fesm/modules/event-bus/event-bus.module.js +84 -0
- package/fesm/modules/event-bus/event-bus.module.spec.js +145 -0
- package/fesm/modules/event-bus/module-events.provider.js +26 -0
- package/fesm/modules/event-bus/module-events.provider.spec.js +52 -0
- package/fesm/modules/index.js +3 -0
- package/fesm/utils/event-pattern.util.js +86 -0
- package/fesm/utils/event-pattern.util.spec.js +230 -0
- package/fesm/utils/index.js +2 -0
- package/fesm/utils/optional-module.util.js +25 -0
- package/fesm/utils/optional-module.util.spec.js +10 -0
- package/interfaces/event-bus.interface.d.ts +74 -0
- package/interfaces/index.d.ts +1 -0
- package/modules/event-bus/event-bus.explorer.d.ts +14 -0
- package/modules/event-bus/event-bus.module.d.ts +8 -0
- package/modules/event-bus/module-events.provider.d.ts +3 -0
- package/modules/index.d.ts +3 -0
- package/package.json +2 -2
- package/utils/event-pattern.util.d.ts +3 -0
- package/utils/index.d.ts +2 -0
- package/utils/optional-module.util.d.ts +1 -0
|
@@ -6,6 +6,8 @@ const _common = require("@nestjs/common");
|
|
|
6
6
|
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
7
7
|
const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
|
|
8
8
|
const _apiserviceclass = require("./api-service.class");
|
|
9
|
+
const _eventbusregistry = require("./event-bus.registry");
|
|
10
|
+
const _eventactionenum = require("../enums/event-action.enum");
|
|
9
11
|
const _utilsservice = require("../modules/utils/utils.service");
|
|
10
12
|
const _entities = require("../entities");
|
|
11
13
|
function _define_property(obj, key, value) {
|
|
@@ -806,4 +808,222 @@ describe('ApiService', ()=>{
|
|
|
806
808
|
expect(clearSpy).toHaveBeenCalledWith('test-entity', mockCache, 'b');
|
|
807
809
|
});
|
|
808
810
|
});
|
|
811
|
+
describe('domain events', ()=>{
|
|
812
|
+
let publish;
|
|
813
|
+
function makeEventService(moduleName = 'test-module') {
|
|
814
|
+
const dataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
815
|
+
return new TestApiService('test-entity', mockCache, utilsService, 'TestApiService', false, moduleName, TestEntity, dataSourceProvider);
|
|
816
|
+
}
|
|
817
|
+
beforeEach(()=>{
|
|
818
|
+
publish = jest.fn().mockResolvedValue(undefined);
|
|
819
|
+
_eventbusregistry.EventBusRegistry.reset();
|
|
820
|
+
_eventbusregistry.EventBusRegistry.setBus({
|
|
821
|
+
publish,
|
|
822
|
+
subscribe: jest.fn(),
|
|
823
|
+
close: jest.fn()
|
|
824
|
+
});
|
|
825
|
+
_eventbusregistry.EventBusRegistry.configureModule('test-module', {
|
|
826
|
+
enabled: true
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
afterEach(()=>{
|
|
830
|
+
_eventbusregistry.EventBusRegistry.reset();
|
|
831
|
+
});
|
|
832
|
+
it('should publish a created event after insert commits', async ()=>{
|
|
833
|
+
const service = makeEventService();
|
|
834
|
+
mockQueryRunner.manager.save.mockResolvedValue({
|
|
835
|
+
id: 'new-1',
|
|
836
|
+
name: 'Widget'
|
|
837
|
+
});
|
|
838
|
+
await service.insert({
|
|
839
|
+
name: 'Widget'
|
|
840
|
+
}, mockUser);
|
|
841
|
+
expect(publish).toHaveBeenCalledTimes(1);
|
|
842
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
843
|
+
name: 'test-module.test-entity.created',
|
|
844
|
+
module: 'test-module',
|
|
845
|
+
entity: 'test-entity',
|
|
846
|
+
action: _eventactionenum.EventAction.CREATED,
|
|
847
|
+
payload: expect.objectContaining({
|
|
848
|
+
ids: [
|
|
849
|
+
'new-1'
|
|
850
|
+
]
|
|
851
|
+
})
|
|
852
|
+
}));
|
|
853
|
+
});
|
|
854
|
+
it('should carry the acting user on the envelope', async ()=>{
|
|
855
|
+
const service = makeEventService();
|
|
856
|
+
mockQueryRunner.manager.save.mockResolvedValue({
|
|
857
|
+
id: 'new-1',
|
|
858
|
+
name: 'Widget'
|
|
859
|
+
});
|
|
860
|
+
await service.insert({
|
|
861
|
+
name: 'Widget'
|
|
862
|
+
}, mockUser);
|
|
863
|
+
expect(publish.mock.calls[0][0].actor).toEqual(expect.objectContaining({
|
|
864
|
+
userId: mockUser.id
|
|
865
|
+
}));
|
|
866
|
+
});
|
|
867
|
+
it('should publish an updated event after update commits', async ()=>{
|
|
868
|
+
const service = makeEventService();
|
|
869
|
+
mockRepo.findOne.mockResolvedValue({
|
|
870
|
+
id: '1',
|
|
871
|
+
name: 'Old'
|
|
872
|
+
});
|
|
873
|
+
mockQueryRunner.manager.save.mockResolvedValue({
|
|
874
|
+
id: '1',
|
|
875
|
+
name: 'New'
|
|
876
|
+
});
|
|
877
|
+
await service.update({
|
|
878
|
+
id: '1',
|
|
879
|
+
name: 'New'
|
|
880
|
+
}, mockUser);
|
|
881
|
+
expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
|
|
882
|
+
action: _eventactionenum.EventAction.UPDATED
|
|
883
|
+
}));
|
|
884
|
+
});
|
|
885
|
+
it('should publish created and updated separately for a bulk upsert', async ()=>{
|
|
886
|
+
const service = makeEventService();
|
|
887
|
+
mockRepo.findOne.mockResolvedValue({
|
|
888
|
+
id: '1',
|
|
889
|
+
name: 'Old'
|
|
890
|
+
});
|
|
891
|
+
mockQueryRunner.manager.save.mockResolvedValueOnce([
|
|
892
|
+
{
|
|
893
|
+
id: 'new-1',
|
|
894
|
+
name: 'A'
|
|
895
|
+
}
|
|
896
|
+
]).mockResolvedValueOnce([
|
|
897
|
+
{
|
|
898
|
+
id: '1',
|
|
899
|
+
name: 'B'
|
|
900
|
+
}
|
|
901
|
+
]);
|
|
902
|
+
await service.bulkUpsert([
|
|
903
|
+
{
|
|
904
|
+
name: 'A'
|
|
905
|
+
}
|
|
906
|
+
], [
|
|
907
|
+
{
|
|
908
|
+
id: '1',
|
|
909
|
+
name: 'B'
|
|
910
|
+
}
|
|
911
|
+
], mockUser);
|
|
912
|
+
expect(publish.mock.calls.map(([event])=>event.action)).toEqual([
|
|
913
|
+
_eventactionenum.EventAction.CREATED,
|
|
914
|
+
_eventactionenum.EventAction.UPDATED
|
|
915
|
+
]);
|
|
916
|
+
});
|
|
917
|
+
it.each([
|
|
918
|
+
[
|
|
919
|
+
'delete',
|
|
920
|
+
_eventactionenum.EventAction.DELETED
|
|
921
|
+
],
|
|
922
|
+
[
|
|
923
|
+
'restore',
|
|
924
|
+
_eventactionenum.EventAction.RESTORED
|
|
925
|
+
],
|
|
926
|
+
[
|
|
927
|
+
'permanent',
|
|
928
|
+
_eventactionenum.EventAction.PURGED
|
|
929
|
+
]
|
|
930
|
+
])('should publish %s as the %s action', async (type, action)=>{
|
|
931
|
+
const service = makeEventService();
|
|
932
|
+
await service.delete({
|
|
933
|
+
id: '1',
|
|
934
|
+
type
|
|
935
|
+
}, mockUser);
|
|
936
|
+
expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
|
|
937
|
+
action
|
|
938
|
+
}));
|
|
939
|
+
});
|
|
940
|
+
it('should carry only the ids for a delete, with no row data to duplicate them', async ()=>{
|
|
941
|
+
const service = makeEventService();
|
|
942
|
+
await service.delete({
|
|
943
|
+
id: '1',
|
|
944
|
+
type: 'delete'
|
|
945
|
+
}, mockUser);
|
|
946
|
+
expect(publish.mock.calls[0][0].payload).toEqual({
|
|
947
|
+
ids: [
|
|
948
|
+
'1'
|
|
949
|
+
]
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
it('should not publish when the module has events disabled', async ()=>{
|
|
953
|
+
_eventbusregistry.EventBusRegistry.configureModule('test-module', {
|
|
954
|
+
enabled: false
|
|
955
|
+
});
|
|
956
|
+
const service = makeEventService();
|
|
957
|
+
mockQueryRunner.manager.save.mockResolvedValue({
|
|
958
|
+
id: 'new-1',
|
|
959
|
+
name: 'Widget'
|
|
960
|
+
});
|
|
961
|
+
await service.insert({
|
|
962
|
+
name: 'Widget'
|
|
963
|
+
}, mockUser);
|
|
964
|
+
expect(publish).not.toHaveBeenCalled();
|
|
965
|
+
});
|
|
966
|
+
it('should not publish for an action the module excluded', async ()=>{
|
|
967
|
+
_eventbusregistry.EventBusRegistry.configureModule('test-module', {
|
|
968
|
+
enabled: true,
|
|
969
|
+
actions: [
|
|
970
|
+
_eventactionenum.EventAction.DELETED
|
|
971
|
+
]
|
|
972
|
+
});
|
|
973
|
+
const service = makeEventService();
|
|
974
|
+
mockQueryRunner.manager.save.mockResolvedValue({
|
|
975
|
+
id: 'new-1',
|
|
976
|
+
name: 'Widget'
|
|
977
|
+
});
|
|
978
|
+
await service.insert({
|
|
979
|
+
name: 'Widget'
|
|
980
|
+
}, mockUser);
|
|
981
|
+
expect(publish).not.toHaveBeenCalled();
|
|
982
|
+
});
|
|
983
|
+
it('should omit the row data when the module opts out of payloads', async ()=>{
|
|
984
|
+
_eventbusregistry.EventBusRegistry.configureModule('test-module', {
|
|
985
|
+
enabled: true,
|
|
986
|
+
includePayload: false
|
|
987
|
+
});
|
|
988
|
+
const service = makeEventService();
|
|
989
|
+
mockQueryRunner.manager.save.mockResolvedValue({
|
|
990
|
+
id: 'new-1',
|
|
991
|
+
name: 'Widget'
|
|
992
|
+
});
|
|
993
|
+
await service.insert({
|
|
994
|
+
name: 'Widget'
|
|
995
|
+
}, mockUser);
|
|
996
|
+
expect(publish.mock.calls[0][0].payload).toEqual({
|
|
997
|
+
ids: [
|
|
998
|
+
'new-1'
|
|
999
|
+
]
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
it('should not fail the write when publishing throws', async ()=>{
|
|
1003
|
+
publish.mockRejectedValue(new Error('broker down'));
|
|
1004
|
+
const service = makeEventService();
|
|
1005
|
+
mockQueryRunner.manager.save.mockResolvedValue({
|
|
1006
|
+
id: 'new-1',
|
|
1007
|
+
name: 'Widget'
|
|
1008
|
+
});
|
|
1009
|
+
await expect(service.insert({
|
|
1010
|
+
name: 'Widget'
|
|
1011
|
+
}, mockUser)).resolves.toEqual({
|
|
1012
|
+
id: 'new-1',
|
|
1013
|
+
name: 'Widget'
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
it('should do nothing when no bus is registered', async ()=>{
|
|
1017
|
+
_eventbusregistry.EventBusRegistry.reset();
|
|
1018
|
+
const service = makeEventService();
|
|
1019
|
+
mockQueryRunner.manager.save.mockResolvedValue({
|
|
1020
|
+
id: 'new-1',
|
|
1021
|
+
name: 'Widget'
|
|
1022
|
+
});
|
|
1023
|
+
await expect(service.insert({
|
|
1024
|
+
name: 'Widget'
|
|
1025
|
+
}, mockUser)).resolves.toBeDefined();
|
|
1026
|
+
expect(publish).not.toHaveBeenCalled();
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
809
1029
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "DisabledEventBus", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return DisabledEventBus;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
let DisabledEventBus = class DisabledEventBus {
|
|
12
|
+
async publish(_event) {}
|
|
13
|
+
async subscribe(_pattern, _handler) {}
|
|
14
|
+
async close() {}
|
|
15
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _disabledeventbusclass = require("./disabled-event-bus.class");
|
|
6
|
+
describe('DisabledEventBus', ()=>{
|
|
7
|
+
const event = {
|
|
8
|
+
id: 'e1',
|
|
9
|
+
name: 'auth.session.logged-in',
|
|
10
|
+
module: 'auth',
|
|
11
|
+
entity: 'session',
|
|
12
|
+
action: 'logged-in',
|
|
13
|
+
payload: {
|
|
14
|
+
ids: [
|
|
15
|
+
'u1'
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
actor: {
|
|
19
|
+
userId: 'u1',
|
|
20
|
+
companyId: null,
|
|
21
|
+
tenantId: null,
|
|
22
|
+
requestId: null
|
|
23
|
+
},
|
|
24
|
+
occurredAt: new Date().toISOString(),
|
|
25
|
+
version: 1
|
|
26
|
+
};
|
|
27
|
+
it('should never deliver an event to a subscriber', async ()=>{
|
|
28
|
+
const bus = new _disabledeventbusclass.DisabledEventBus();
|
|
29
|
+
const handler = jest.fn();
|
|
30
|
+
await bus.subscribe('**', handler);
|
|
31
|
+
await bus.publish(event);
|
|
32
|
+
expect(handler).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
it('should resolve without throwing so callers need no special case', async ()=>{
|
|
35
|
+
const bus = new _disabledeventbusclass.DisabledEventBus();
|
|
36
|
+
await expect(bus.publish(event)).resolves.toBeUndefined();
|
|
37
|
+
await expect(bus.subscribe('**', jest.fn())).resolves.toBeUndefined();
|
|
38
|
+
await expect(bus.close()).resolves.toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get DOMAIN_EVENT_VERSION () {
|
|
13
|
+
return DOMAIN_EVENT_VERSION;
|
|
14
|
+
},
|
|
15
|
+
get buildDomainEvent () {
|
|
16
|
+
return buildDomainEvent;
|
|
17
|
+
},
|
|
18
|
+
get publishDomainEvent () {
|
|
19
|
+
return publishDomainEvent;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const _crypto = require("crypto");
|
|
23
|
+
const _loggermiddleware = require("../middlewares/logger.middleware");
|
|
24
|
+
const _eventpatternutil = require("../utils/event-pattern.util");
|
|
25
|
+
const _eventbusregistry = require("./event-bus.registry");
|
|
26
|
+
const DOMAIN_EVENT_VERSION = 1;
|
|
27
|
+
function buildDomainEvent(input) {
|
|
28
|
+
const rows = input.data === undefined ? [] : toArray(input.data);
|
|
29
|
+
const ids = input.ids ?? collectIds(rows);
|
|
30
|
+
return {
|
|
31
|
+
id: (0, _crypto.randomUUID)(),
|
|
32
|
+
name: `${input.module}.${input.entity}.${input.action}`,
|
|
33
|
+
module: input.module,
|
|
34
|
+
entity: input.entity,
|
|
35
|
+
action: input.action,
|
|
36
|
+
payload: {
|
|
37
|
+
ids,
|
|
38
|
+
...rows.length && _eventbusregistry.EventBusRegistry.shouldIncludePayload(input.module) ? {
|
|
39
|
+
data: (0, _eventpatternutil.sanitizeEventPayload)(rows)
|
|
40
|
+
} : {},
|
|
41
|
+
...input.metadata ? {
|
|
42
|
+
metadata: (0, _eventpatternutil.sanitizeEventPayload)(input.metadata)
|
|
43
|
+
} : {}
|
|
44
|
+
},
|
|
45
|
+
actor: {
|
|
46
|
+
userId: input.user?.id ?? null,
|
|
47
|
+
companyId: input.user?.companyId ?? (0, _loggermiddleware.getCompanyId)() ?? null,
|
|
48
|
+
tenantId: (0, _loggermiddleware.getTenantId)() ?? null,
|
|
49
|
+
requestId: (0, _loggermiddleware.getRequestId)() ?? null
|
|
50
|
+
},
|
|
51
|
+
occurredAt: new Date().toISOString(),
|
|
52
|
+
version: DOMAIN_EVENT_VERSION
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function publishDomainEvent(input) {
|
|
56
|
+
const bus = input.eventBus ?? _eventbusregistry.EventBusRegistry.getBus();
|
|
57
|
+
if (!bus) return;
|
|
58
|
+
if (!_eventbusregistry.EventBusRegistry.isEnabled(input.module, input.entity, input.action)) return;
|
|
59
|
+
try {
|
|
60
|
+
await bus.publish(buildDomainEvent(input));
|
|
61
|
+
} catch {
|
|
62
|
+
// Events must never break the operation that produced them
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function toArray(value) {
|
|
66
|
+
return Array.isArray(value) ? value : [
|
|
67
|
+
value
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
function collectIds(rows) {
|
|
71
|
+
return rows.reduce((ids, row)=>{
|
|
72
|
+
const id = row?.id;
|
|
73
|
+
if (id) ids.push(id);
|
|
74
|
+
return ids;
|
|
75
|
+
}, []);
|
|
76
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _domaineventpublisher = require("./domain-event.publisher");
|
|
6
|
+
const _eventbusregistry = require("./event-bus.registry");
|
|
7
|
+
const user = {
|
|
8
|
+
id: 'user-1',
|
|
9
|
+
email: 'ada@example.com',
|
|
10
|
+
companyId: 'company-1'
|
|
11
|
+
};
|
|
12
|
+
describe('buildDomainEvent', ()=>{
|
|
13
|
+
beforeEach(()=>{
|
|
14
|
+
_eventbusregistry.EventBusRegistry.reset();
|
|
15
|
+
_eventbusregistry.EventBusRegistry.configureModule('form-builder', {
|
|
16
|
+
enabled: true
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
afterEach(()=>{
|
|
20
|
+
_eventbusregistry.EventBusRegistry.reset();
|
|
21
|
+
});
|
|
22
|
+
it('should name the event <module>.<entity>.<action>', ()=>{
|
|
23
|
+
const event = (0, _domaineventpublisher.buildDomainEvent)({
|
|
24
|
+
module: 'form-builder',
|
|
25
|
+
entity: 'form_result',
|
|
26
|
+
action: 'submitted'
|
|
27
|
+
});
|
|
28
|
+
expect(event.name).toBe('form-builder.form_result.submitted');
|
|
29
|
+
expect(event.version).toBe(1);
|
|
30
|
+
expect(event.id).toEqual(expect.any(String));
|
|
31
|
+
});
|
|
32
|
+
it('should derive ids from the supplied rows when none are given', ()=>{
|
|
33
|
+
const event = (0, _domaineventpublisher.buildDomainEvent)({
|
|
34
|
+
module: 'form-builder',
|
|
35
|
+
entity: 'form_result',
|
|
36
|
+
action: 'submitted',
|
|
37
|
+
data: [
|
|
38
|
+
{
|
|
39
|
+
id: 'a'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'b'
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
});
|
|
46
|
+
expect(event.payload.ids).toEqual([
|
|
47
|
+
'a',
|
|
48
|
+
'b'
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
it('should prefer explicitly supplied ids', ()=>{
|
|
52
|
+
const event = (0, _domaineventpublisher.buildDomainEvent)({
|
|
53
|
+
module: 'form-builder',
|
|
54
|
+
entity: 'form_result',
|
|
55
|
+
action: 'submitted',
|
|
56
|
+
ids: [
|
|
57
|
+
'explicit'
|
|
58
|
+
],
|
|
59
|
+
data: {
|
|
60
|
+
id: 'ignored'
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
expect(event.payload.ids).toEqual([
|
|
64
|
+
'explicit'
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
it('should attach metadata regardless of the includePayload setting', ()=>{
|
|
68
|
+
_eventbusregistry.EventBusRegistry.configureModule('form-builder', {
|
|
69
|
+
enabled: true,
|
|
70
|
+
includePayload: false
|
|
71
|
+
});
|
|
72
|
+
const event = (0, _domaineventpublisher.buildDomainEvent)({
|
|
73
|
+
module: 'form-builder',
|
|
74
|
+
entity: 'form_result',
|
|
75
|
+
action: 'submitted',
|
|
76
|
+
data: {
|
|
77
|
+
id: 'a',
|
|
78
|
+
answer: 'secret-ish'
|
|
79
|
+
},
|
|
80
|
+
metadata: {
|
|
81
|
+
formId: 'form-1',
|
|
82
|
+
isDraft: false
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
expect(event.payload.data).toBeUndefined();
|
|
86
|
+
expect(event.payload.metadata).toEqual({
|
|
87
|
+
formId: 'form-1',
|
|
88
|
+
isDraft: false
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
it('should redact credential-like fields from data and metadata', ()=>{
|
|
92
|
+
const event = (0, _domaineventpublisher.buildDomainEvent)({
|
|
93
|
+
module: 'form-builder',
|
|
94
|
+
entity: 'form_result',
|
|
95
|
+
action: 'submitted',
|
|
96
|
+
data: {
|
|
97
|
+
id: 'a',
|
|
98
|
+
password: 'hunter2'
|
|
99
|
+
},
|
|
100
|
+
metadata: {
|
|
101
|
+
token: 'abc'
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
expect(event.payload.data).toEqual([
|
|
105
|
+
{
|
|
106
|
+
id: 'a',
|
|
107
|
+
password: '[REDACTED]'
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
expect(event.payload.metadata).toEqual({
|
|
111
|
+
token: '[REDACTED]'
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
it('should record the acting user', ()=>{
|
|
115
|
+
const event = (0, _domaineventpublisher.buildDomainEvent)({
|
|
116
|
+
module: 'form-builder',
|
|
117
|
+
entity: 'form_result',
|
|
118
|
+
action: 'submitted',
|
|
119
|
+
user
|
|
120
|
+
});
|
|
121
|
+
expect(event.actor).toEqual(expect.objectContaining({
|
|
122
|
+
userId: 'user-1',
|
|
123
|
+
companyId: 'company-1'
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe('publishDomainEvent', ()=>{
|
|
128
|
+
let publish;
|
|
129
|
+
beforeEach(()=>{
|
|
130
|
+
publish = jest.fn().mockResolvedValue(undefined);
|
|
131
|
+
_eventbusregistry.EventBusRegistry.reset();
|
|
132
|
+
_eventbusregistry.EventBusRegistry.setBus({
|
|
133
|
+
publish,
|
|
134
|
+
subscribe: jest.fn(),
|
|
135
|
+
close: jest.fn()
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
afterEach(()=>{
|
|
139
|
+
_eventbusregistry.EventBusRegistry.reset();
|
|
140
|
+
});
|
|
141
|
+
it('should publish when the module has events enabled', async ()=>{
|
|
142
|
+
_eventbusregistry.EventBusRegistry.configureModule('auth', {
|
|
143
|
+
enabled: true
|
|
144
|
+
});
|
|
145
|
+
await (0, _domaineventpublisher.publishDomainEvent)({
|
|
146
|
+
module: 'auth',
|
|
147
|
+
entity: 'session',
|
|
148
|
+
action: 'logged-in'
|
|
149
|
+
});
|
|
150
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
151
|
+
name: 'auth.session.logged-in'
|
|
152
|
+
}));
|
|
153
|
+
});
|
|
154
|
+
it('should stay silent when the module has events disabled', async ()=>{
|
|
155
|
+
_eventbusregistry.EventBusRegistry.configureModule('auth', {
|
|
156
|
+
enabled: false
|
|
157
|
+
});
|
|
158
|
+
await (0, _domaineventpublisher.publishDomainEvent)({
|
|
159
|
+
module: 'auth',
|
|
160
|
+
entity: 'session',
|
|
161
|
+
action: 'logged-in'
|
|
162
|
+
});
|
|
163
|
+
expect(publish).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
it('should honour an action allowlist that names a domain action', async ()=>{
|
|
166
|
+
_eventbusregistry.EventBusRegistry.configureModule('auth', {
|
|
167
|
+
enabled: true,
|
|
168
|
+
actions: [
|
|
169
|
+
'logged-in'
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
await (0, _domaineventpublisher.publishDomainEvent)({
|
|
173
|
+
module: 'auth',
|
|
174
|
+
entity: 'session',
|
|
175
|
+
action: 'logged-in'
|
|
176
|
+
});
|
|
177
|
+
await (0, _domaineventpublisher.publishDomainEvent)({
|
|
178
|
+
module: 'auth',
|
|
179
|
+
entity: 'session',
|
|
180
|
+
action: 'logged-out'
|
|
181
|
+
});
|
|
182
|
+
expect(publish).toHaveBeenCalledTimes(1);
|
|
183
|
+
});
|
|
184
|
+
it('should publish through an explicitly supplied bus instead of the registered one', async ()=>{
|
|
185
|
+
const directPublish = jest.fn().mockResolvedValue(undefined);
|
|
186
|
+
_eventbusregistry.EventBusRegistry.configureModule('auth', {
|
|
187
|
+
enabled: true
|
|
188
|
+
});
|
|
189
|
+
await (0, _domaineventpublisher.publishDomainEvent)({
|
|
190
|
+
module: 'auth',
|
|
191
|
+
entity: 'session',
|
|
192
|
+
action: 'logged-in',
|
|
193
|
+
eventBus: {
|
|
194
|
+
publish: directPublish
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
expect(directPublish).toHaveBeenCalled();
|
|
198
|
+
expect(publish).not.toHaveBeenCalled();
|
|
199
|
+
});
|
|
200
|
+
it('should still respect the module switch when a bus is supplied explicitly', async ()=>{
|
|
201
|
+
const directPublish = jest.fn().mockResolvedValue(undefined);
|
|
202
|
+
_eventbusregistry.EventBusRegistry.configureModule('auth', {
|
|
203
|
+
enabled: false
|
|
204
|
+
});
|
|
205
|
+
await (0, _domaineventpublisher.publishDomainEvent)({
|
|
206
|
+
module: 'auth',
|
|
207
|
+
entity: 'session',
|
|
208
|
+
action: 'logged-in',
|
|
209
|
+
eventBus: {
|
|
210
|
+
publish: directPublish
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
expect(directPublish).not.toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
it('should do nothing when no bus is registered', async ()=>{
|
|
216
|
+
_eventbusregistry.EventBusRegistry.reset();
|
|
217
|
+
await expect((0, _domaineventpublisher.publishDomainEvent)({
|
|
218
|
+
module: 'auth',
|
|
219
|
+
entity: 'session',
|
|
220
|
+
action: 'logged-in'
|
|
221
|
+
})).resolves.toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
it('should swallow a broker failure', async ()=>{
|
|
224
|
+
_eventbusregistry.EventBusRegistry.configureModule('auth', {
|
|
225
|
+
enabled: true
|
|
226
|
+
});
|
|
227
|
+
publish.mockRejectedValue(new Error('broker down'));
|
|
228
|
+
await expect((0, _domaineventpublisher.publishDomainEvent)({
|
|
229
|
+
module: 'auth',
|
|
230
|
+
entity: 'session',
|
|
231
|
+
action: 'logged-in'
|
|
232
|
+
})).resolves.toBeUndefined();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "EventBusRegistry", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return EventBusRegistry;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
function _define_property(obj, key, value) {
|
|
12
|
+
if (key in obj) {
|
|
13
|
+
Object.defineProperty(obj, key, {
|
|
14
|
+
value: value,
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
obj[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return obj;
|
|
23
|
+
}
|
|
24
|
+
const DISABLED = {
|
|
25
|
+
enabled: false
|
|
26
|
+
};
|
|
27
|
+
let EventBusRegistry = class EventBusRegistry {
|
|
28
|
+
static setBus(bus) {
|
|
29
|
+
this.bus = bus;
|
|
30
|
+
}
|
|
31
|
+
static getBus() {
|
|
32
|
+
return this.bus;
|
|
33
|
+
}
|
|
34
|
+
static setDefaultModuleEvents(config) {
|
|
35
|
+
this.defaultConfig = config ?? DISABLED;
|
|
36
|
+
}
|
|
37
|
+
/** Called by each feature module with the `events` block from its own config */ static configureModule(moduleName, config) {
|
|
38
|
+
this.moduleConfigs.set(moduleName, config ?? DISABLED);
|
|
39
|
+
}
|
|
40
|
+
static getModuleConfig(moduleName) {
|
|
41
|
+
if (!moduleName) return this.defaultConfig;
|
|
42
|
+
return this.moduleConfigs.get(moduleName) ?? this.defaultConfig;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Answers only "is this module configured to publish this" - whether a bus
|
|
46
|
+
* exists is the publisher's concern, so an explicitly supplied bus is still
|
|
47
|
+
* held to the module's own on/off switch.
|
|
48
|
+
*/ static isEnabled(moduleName, entity, action) {
|
|
49
|
+
const config = this.getModuleConfig(moduleName);
|
|
50
|
+
if (!config.enabled) return false;
|
|
51
|
+
if (config.excludeEntities?.includes(entity)) return false;
|
|
52
|
+
if (config.entities?.length && !config.entities.includes(entity)) return false;
|
|
53
|
+
// Without an explicit allowlist, CRUD lifecycle actions and every domain
|
|
54
|
+
// action a package defines are all published.
|
|
55
|
+
if (!config.actions?.length) return true;
|
|
56
|
+
return config.actions.includes(action);
|
|
57
|
+
}
|
|
58
|
+
static shouldIncludePayload(moduleName) {
|
|
59
|
+
return this.getModuleConfig(moduleName).includePayload ?? true;
|
|
60
|
+
}
|
|
61
|
+
static reset() {
|
|
62
|
+
this.bus = undefined;
|
|
63
|
+
this.moduleConfigs.clear();
|
|
64
|
+
this.defaultConfig = DISABLED;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
_define_property(EventBusRegistry, "bus", void 0);
|
|
68
|
+
_define_property(EventBusRegistry, "moduleConfigs", new Map());
|
|
69
|
+
_define_property(EventBusRegistry, "defaultConfig", DISABLED);
|