@flusys/nestjs-shared 7.0.0-beta.1 → 7.0.1
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 +132 -84
- package/cjs/classes/api-service.class.js +37 -47
- package/cjs/classes/api-service.class.spec.js +120 -12
- package/cjs/classes/event-bus.registry.js +2 -3
- package/cjs/classes/event-bus.registry.spec.js +49 -24
- package/cjs/classes/event-transports/kafka-transport.optional.spec.js +1 -2
- package/cjs/classes/event-transports/memory-transport.js +3 -4
- package/cjs/classes/event-transports/memory-transport.spec.js +1 -2
- package/cjs/classes/event-transports/rabbitmq-transport.optional.spec.js +1 -2
- package/cjs/classes/hybrid-event-bus.class.js +23 -3
- package/cjs/classes/hybrid-event-bus.class.spec.js +31 -2
- package/cjs/enums/index.js +0 -1
- package/cjs/modules/event-bus/event-bus.integration.spec.js +1 -2
- package/cjs/utils/event-pattern.util.js +6 -1
- package/cjs/utils/event-pattern.util.spec.js +20 -3
- package/classes/api-service.class.d.ts +8 -12
- package/classes/domain-event.publisher.d.ts +2 -2
- package/classes/event-bus.registry.d.ts +2 -2
- package/classes/event-transports/memory-transport.d.ts +1 -1
- package/classes/hybrid-event-bus.class.d.ts +2 -0
- package/enums/index.d.ts +0 -1
- package/fesm/classes/api-service.class.js +37 -47
- package/fesm/classes/api-service.class.spec.js +120 -12
- package/fesm/classes/event-bus.registry.js +2 -3
- package/fesm/classes/event-bus.registry.spec.js +49 -24
- package/fesm/classes/event-transports/kafka-transport.optional.spec.js +1 -2
- package/fesm/classes/event-transports/memory-transport.js +7 -4
- package/fesm/classes/event-transports/memory-transport.spec.js +1 -2
- package/fesm/classes/event-transports/rabbitmq-transport.optional.spec.js +1 -2
- package/fesm/classes/hybrid-event-bus.class.js +23 -3
- package/fesm/classes/hybrid-event-bus.class.spec.js +31 -2
- package/fesm/enums/index.js +0 -1
- package/fesm/modules/event-bus/event-bus.integration.spec.js +1 -2
- package/fesm/utils/event-pattern.util.js +6 -1
- package/fesm/utils/event-pattern.util.spec.js +20 -3
- package/interfaces/event-bus.interface.d.ts +3 -6
- package/package.json +2 -2
- package/cjs/enums/event-action.enum.js +0 -18
- package/enums/event-action.enum.d.ts +0 -7
- package/fesm/enums/event-action.enum.js +0 -8
|
@@ -7,7 +7,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
9
|
const _eventbusregistry = require("./event-bus.registry");
|
|
10
|
-
const _eventactionenum = require("../enums/event-action.enum");
|
|
11
10
|
const _utilsservice = require("../modules/utils/utils.service");
|
|
12
11
|
const _entities = require("../entities");
|
|
13
12
|
function _define_property(obj, key, value) {
|
|
@@ -29,6 +28,14 @@ let TestEntity = class TestEntity extends _entities.Identity {
|
|
|
29
28
|
}
|
|
30
29
|
};
|
|
31
30
|
let TestApiService = class TestApiService extends _apiserviceclass.ApiService {
|
|
31
|
+
/** `publishDomainAction` is protected - feature services reach it as subclasses do */ publishAction(action, metadata) {
|
|
32
|
+
return this.publishDomainAction(action, {
|
|
33
|
+
ids: [
|
|
34
|
+
'1'
|
|
35
|
+
],
|
|
36
|
+
metadata
|
|
37
|
+
});
|
|
38
|
+
}
|
|
32
39
|
};
|
|
33
40
|
function createMockQueryRunner() {
|
|
34
41
|
return {
|
|
@@ -56,7 +63,7 @@ describe('ApiService', ()=>{
|
|
|
56
63
|
let errorLogSpy;
|
|
57
64
|
function makeService(isCacheable = false) {
|
|
58
65
|
const dataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
59
|
-
return new TestApiService('test-entity', mockCache, utilsService, 'TestApiService', isCacheable,
|
|
66
|
+
return new TestApiService('test-entity', mockCache, utilsService, 'TestApiService', isCacheable, 'app', dataSourceProvider, TestEntity);
|
|
60
67
|
}
|
|
61
68
|
beforeEach(()=>{
|
|
62
69
|
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
@@ -645,7 +652,7 @@ describe('ApiService', ()=>{
|
|
|
645
652
|
function makeRawService(dbType) {
|
|
646
653
|
mockRepo.manager.connection.options.type = dbType;
|
|
647
654
|
const dataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
648
|
-
return new RawTestApiService('test-entity', mockCache, utilsService, 'RawTestApiService', false,
|
|
655
|
+
return new RawTestApiService('test-entity', mockCache, utilsService, 'RawTestApiService', false, 'app', dataSourceProvider, TestEntity);
|
|
649
656
|
}
|
|
650
657
|
function primeRawQueryBuilder(qb) {
|
|
651
658
|
qb.expressionMap = {
|
|
@@ -812,7 +819,7 @@ describe('ApiService', ()=>{
|
|
|
812
819
|
let publish;
|
|
813
820
|
function makeEventService(moduleName = 'test-module') {
|
|
814
821
|
const dataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
815
|
-
return new TestApiService('test-entity', mockCache, utilsService, 'TestApiService', false, moduleName,
|
|
822
|
+
return new TestApiService('test-entity', mockCache, utilsService, 'TestApiService', false, moduleName, dataSourceProvider, TestEntity);
|
|
816
823
|
}
|
|
817
824
|
beforeEach(()=>{
|
|
818
825
|
publish = jest.fn().mockResolvedValue(undefined);
|
|
@@ -843,7 +850,7 @@ describe('ApiService', ()=>{
|
|
|
843
850
|
name: 'test-module.test-entity.created',
|
|
844
851
|
module: 'test-module',
|
|
845
852
|
entity: 'test-entity',
|
|
846
|
-
action:
|
|
853
|
+
action: 'created',
|
|
847
854
|
payload: expect.objectContaining({
|
|
848
855
|
ids: [
|
|
849
856
|
'new-1'
|
|
@@ -864,6 +871,74 @@ describe('ApiService', ()=>{
|
|
|
864
871
|
userId: mockUser.id
|
|
865
872
|
}));
|
|
866
873
|
});
|
|
874
|
+
it('should publish one created event carrying every id for insertMany', async ()=>{
|
|
875
|
+
const service = makeEventService();
|
|
876
|
+
mockQueryRunner.manager.save.mockResolvedValue([
|
|
877
|
+
{
|
|
878
|
+
id: 'new-1',
|
|
879
|
+
name: 'A'
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
id: 'new-2',
|
|
883
|
+
name: 'B'
|
|
884
|
+
}
|
|
885
|
+
]);
|
|
886
|
+
await service.insertMany([
|
|
887
|
+
{
|
|
888
|
+
name: 'A'
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
name: 'B'
|
|
892
|
+
}
|
|
893
|
+
], mockUser);
|
|
894
|
+
expect(publish).toHaveBeenCalledTimes(1);
|
|
895
|
+
expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
|
|
896
|
+
action: 'created',
|
|
897
|
+
payload: expect.objectContaining({
|
|
898
|
+
ids: [
|
|
899
|
+
'new-1',
|
|
900
|
+
'new-2'
|
|
901
|
+
]
|
|
902
|
+
})
|
|
903
|
+
}));
|
|
904
|
+
});
|
|
905
|
+
it('should publish one updated event carrying every id for updateMany', async ()=>{
|
|
906
|
+
const service = makeEventService();
|
|
907
|
+
mockRepo.findOne.mockResolvedValue({
|
|
908
|
+
id: '1',
|
|
909
|
+
name: 'Old'
|
|
910
|
+
});
|
|
911
|
+
mockQueryRunner.manager.save.mockResolvedValue([
|
|
912
|
+
{
|
|
913
|
+
id: '1',
|
|
914
|
+
name: 'A'
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
id: '2',
|
|
918
|
+
name: 'B'
|
|
919
|
+
}
|
|
920
|
+
]);
|
|
921
|
+
await service.updateMany([
|
|
922
|
+
{
|
|
923
|
+
id: '1',
|
|
924
|
+
name: 'A'
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
id: '2',
|
|
928
|
+
name: 'B'
|
|
929
|
+
}
|
|
930
|
+
], mockUser);
|
|
931
|
+
expect(publish).toHaveBeenCalledTimes(1);
|
|
932
|
+
expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
|
|
933
|
+
action: 'updated',
|
|
934
|
+
payload: expect.objectContaining({
|
|
935
|
+
ids: [
|
|
936
|
+
'1',
|
|
937
|
+
'2'
|
|
938
|
+
]
|
|
939
|
+
})
|
|
940
|
+
}));
|
|
941
|
+
});
|
|
867
942
|
it('should publish an updated event after update commits', async ()=>{
|
|
868
943
|
const service = makeEventService();
|
|
869
944
|
mockRepo.findOne.mockResolvedValue({
|
|
@@ -879,7 +954,7 @@ describe('ApiService', ()=>{
|
|
|
879
954
|
name: 'New'
|
|
880
955
|
}, mockUser);
|
|
881
956
|
expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
|
|
882
|
-
action:
|
|
957
|
+
action: 'updated'
|
|
883
958
|
}));
|
|
884
959
|
});
|
|
885
960
|
it('should publish created and updated separately for a bulk upsert', async ()=>{
|
|
@@ -910,22 +985,22 @@ describe('ApiService', ()=>{
|
|
|
910
985
|
}
|
|
911
986
|
], mockUser);
|
|
912
987
|
expect(publish.mock.calls.map(([event])=>event.action)).toEqual([
|
|
913
|
-
|
|
914
|
-
|
|
988
|
+
'created',
|
|
989
|
+
'updated'
|
|
915
990
|
]);
|
|
916
991
|
});
|
|
917
992
|
it.each([
|
|
918
993
|
[
|
|
919
994
|
'delete',
|
|
920
|
-
|
|
995
|
+
'deleted'
|
|
921
996
|
],
|
|
922
997
|
[
|
|
923
998
|
'restore',
|
|
924
|
-
|
|
999
|
+
'restored'
|
|
925
1000
|
],
|
|
926
1001
|
[
|
|
927
1002
|
'permanent',
|
|
928
|
-
|
|
1003
|
+
'purged'
|
|
929
1004
|
]
|
|
930
1005
|
])('should publish %s as the %s action', async (type, action)=>{
|
|
931
1006
|
const service = makeEventService();
|
|
@@ -949,6 +1024,39 @@ describe('ApiService', ()=>{
|
|
|
949
1024
|
]
|
|
950
1025
|
});
|
|
951
1026
|
});
|
|
1027
|
+
it('should publish a domain action under this service module and entity', async ()=>{
|
|
1028
|
+
const service = makeEventService();
|
|
1029
|
+
await service.publishAction('submitted', {
|
|
1030
|
+
formId: 'form-1'
|
|
1031
|
+
});
|
|
1032
|
+
expect(publish.mock.calls[0][0]).toEqual(expect.objectContaining({
|
|
1033
|
+
name: 'test-module.test-entity.submitted',
|
|
1034
|
+
module: 'test-module',
|
|
1035
|
+
entity: 'test-entity',
|
|
1036
|
+
action: 'submitted',
|
|
1037
|
+
payload: {
|
|
1038
|
+
ids: [
|
|
1039
|
+
'1'
|
|
1040
|
+
],
|
|
1041
|
+
metadata: {
|
|
1042
|
+
formId: 'form-1'
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}));
|
|
1046
|
+
});
|
|
1047
|
+
it('should hold a domain action to the same allowlist as the CRUD actions', async ()=>{
|
|
1048
|
+
_eventbusregistry.EventBusRegistry.configureModule('test-module', {
|
|
1049
|
+
enabled: true,
|
|
1050
|
+
actions: [
|
|
1051
|
+
'test-entity.submitted'
|
|
1052
|
+
]
|
|
1053
|
+
});
|
|
1054
|
+
const service = makeEventService();
|
|
1055
|
+
await service.publishAction('submitted');
|
|
1056
|
+
await service.publishAction('draft-saved');
|
|
1057
|
+
expect(publish).toHaveBeenCalledTimes(1);
|
|
1058
|
+
expect(publish.mock.calls[0][0].action).toBe('submitted');
|
|
1059
|
+
});
|
|
952
1060
|
it('should not publish when the module has events disabled', async ()=>{
|
|
953
1061
|
_eventbusregistry.EventBusRegistry.configureModule('test-module', {
|
|
954
1062
|
enabled: false
|
|
@@ -967,7 +1075,7 @@ describe('ApiService', ()=>{
|
|
|
967
1075
|
_eventbusregistry.EventBusRegistry.configureModule('test-module', {
|
|
968
1076
|
enabled: true,
|
|
969
1077
|
actions: [
|
|
970
|
-
|
|
1078
|
+
'deleted'
|
|
971
1079
|
]
|
|
972
1080
|
});
|
|
973
1081
|
const service = makeEventService();
|
|
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "EventBusRegistry", {
|
|
|
8
8
|
return EventBusRegistry;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
+
const _eventpatternutil = require("../utils/event-pattern.util");
|
|
11
12
|
function _define_property(obj, key, value) {
|
|
12
13
|
if (key in obj) {
|
|
13
14
|
Object.defineProperty(obj, key, {
|
|
@@ -48,12 +49,10 @@ let EventBusRegistry = class EventBusRegistry {
|
|
|
48
49
|
*/ static isEnabled(moduleName, entity, action) {
|
|
49
50
|
const config = this.getModuleConfig(moduleName);
|
|
50
51
|
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
52
|
// Without an explicit allowlist, CRUD lifecycle actions and every domain
|
|
54
53
|
// action a package defines are all published.
|
|
55
54
|
if (!config.actions?.length) return true;
|
|
56
|
-
return config.actions.includes(action);
|
|
55
|
+
return config.actions.some((entry)=>entry.includes('.') ? (0, _eventpatternutil.matchEventPattern)(entry, `${entity}.${action}`) : (0, _eventpatternutil.matchEventPattern)(entry, action));
|
|
57
56
|
}
|
|
58
57
|
static shouldIncludePayload(moduleName) {
|
|
59
58
|
return this.getModuleConfig(moduleName).includePayload ?? true;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", {
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
|
-
const _eventactionenum = require("../enums/event-action.enum");
|
|
6
5
|
const _eventbusregistry = require("./event-bus.registry");
|
|
7
6
|
const bus = {
|
|
8
7
|
publish: jest.fn(),
|
|
@@ -26,39 +25,39 @@ describe('EventBusRegistry', ()=>{
|
|
|
26
25
|
enabled: true
|
|
27
26
|
});
|
|
28
27
|
expect(_eventbusregistry.EventBusRegistry.getBus()).toBeUndefined();
|
|
29
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder',
|
|
28
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder', 'created')).toBe(true);
|
|
30
29
|
});
|
|
31
30
|
it('should report disabled for a module that never registered a config', ()=>{
|
|
32
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder',
|
|
31
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder', 'created')).toBe(false);
|
|
33
32
|
});
|
|
34
33
|
it('should enable every action of an enabled module by default', ()=>{
|
|
35
34
|
_eventbusregistry.EventBusRegistry.configureModule('storage', {
|
|
36
35
|
enabled: true
|
|
37
36
|
});
|
|
38
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder',
|
|
39
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder',
|
|
37
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder', 'created')).toBe(true);
|
|
38
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder', 'deleted')).toBe(true);
|
|
40
39
|
});
|
|
41
40
|
it('should honour an action allowlist', ()=>{
|
|
42
41
|
_eventbusregistry.EventBusRegistry.configureModule('storage', {
|
|
43
42
|
enabled: true,
|
|
44
43
|
actions: [
|
|
45
|
-
|
|
44
|
+
'created'
|
|
46
45
|
]
|
|
47
46
|
});
|
|
48
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder',
|
|
49
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder',
|
|
47
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder', 'created')).toBe(true);
|
|
48
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder', 'updated')).toBe(false);
|
|
50
49
|
});
|
|
51
50
|
it('should honour an allowlist that mixes CRUD lifecycle and domain actions', ()=>{
|
|
52
51
|
_eventbusregistry.EventBusRegistry.configureModule('auth', {
|
|
53
52
|
enabled: true,
|
|
54
53
|
actions: [
|
|
55
|
-
|
|
54
|
+
'created',
|
|
56
55
|
'logged-in'
|
|
57
56
|
]
|
|
58
57
|
});
|
|
59
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'appuser',
|
|
58
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'appuser', 'created')).toBe(true);
|
|
60
59
|
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'session', 'logged-in')).toBe(true);
|
|
61
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'appuser',
|
|
60
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'appuser', 'updated')).toBe(false);
|
|
62
61
|
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'session', 'logged-out')).toBe(false);
|
|
63
62
|
});
|
|
64
63
|
it('should silence CRUD when the allowlist names only domain actions', ()=>{
|
|
@@ -69,28 +68,54 @@ describe('EventBusRegistry', ()=>{
|
|
|
69
68
|
]
|
|
70
69
|
});
|
|
71
70
|
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'session', 'logged-in')).toBe(true);
|
|
72
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'appuser',
|
|
71
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'appuser', 'created')).toBe(false);
|
|
73
72
|
});
|
|
74
|
-
it('should
|
|
75
|
-
_eventbusregistry.EventBusRegistry.configureModule('
|
|
73
|
+
it('should scope an action to one entity when the entry names it', ()=>{
|
|
74
|
+
_eventbusregistry.EventBusRegistry.configureModule('iam', {
|
|
75
|
+
enabled: true,
|
|
76
|
+
actions: [
|
|
77
|
+
'created',
|
|
78
|
+
'role.updated'
|
|
79
|
+
]
|
|
80
|
+
});
|
|
81
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('iam', 'role', 'updated')).toBe(true);
|
|
82
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('iam', 'action', 'updated')).toBe(false);
|
|
83
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('iam', 'action', 'created')).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
it('should treat a scoped entry as a routing pattern', ()=>{
|
|
86
|
+
_eventbusregistry.EventBusRegistry.configureModule('iam', {
|
|
76
87
|
enabled: true,
|
|
77
|
-
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
],
|
|
81
|
-
excludeEntities: [
|
|
82
|
-
'file_manager'
|
|
88
|
+
actions: [
|
|
89
|
+
'role.*',
|
|
90
|
+
'*.purged'
|
|
83
91
|
]
|
|
84
92
|
});
|
|
85
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('
|
|
86
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('
|
|
87
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('
|
|
93
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('iam', 'role', 'updated')).toBe(true);
|
|
94
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('iam', 'role', 'permissions-assigned')).toBe(true);
|
|
95
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('iam', 'action', 'purged')).toBe(true);
|
|
96
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('iam', 'action', 'updated')).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
it('should publish only the entity and action pairs an app lists', ()=>{
|
|
99
|
+
_eventbusregistry.EventBusRegistry.configureModule('auth', {
|
|
100
|
+
enabled: true,
|
|
101
|
+
actions: [
|
|
102
|
+
'company.created',
|
|
103
|
+
'company.updated',
|
|
104
|
+
'session.logged-in'
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'company', 'created')).toBe(true);
|
|
108
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'company', 'updated')).toBe(true);
|
|
109
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'company', 'purged')).toBe(false);
|
|
110
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'appuser', 'created')).toBe(false);
|
|
111
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'session', 'logged-in')).toBe(true);
|
|
112
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'session', 'logged-out')).toBe(false);
|
|
88
113
|
});
|
|
89
114
|
it('should fall back to the default config for unregistered modules', ()=>{
|
|
90
115
|
_eventbusregistry.EventBusRegistry.setDefaultModuleEvents({
|
|
91
116
|
enabled: true
|
|
92
117
|
});
|
|
93
|
-
expect(_eventbusregistry.EventBusRegistry.isEnabled('unknown-module', 'thing',
|
|
118
|
+
expect(_eventbusregistry.EventBusRegistry.isEnabled('unknown-module', 'thing', 'created')).toBe(true);
|
|
94
119
|
});
|
|
95
120
|
it('should include the payload unless a module opts out', ()=>{
|
|
96
121
|
_eventbusregistry.EventBusRegistry.configureModule('storage', {
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
const _common = require("@nestjs/common");
|
|
6
|
-
const _eventactionenum = require("../../enums/event-action.enum");
|
|
7
6
|
const _kafkatransportoptional = require("./kafka-transport.optional");
|
|
8
7
|
const mockLoadOptionalModule = jest.fn();
|
|
9
8
|
jest.mock('../../utils/optional-module.util', ()=>({
|
|
@@ -15,7 +14,7 @@ function buildEvent() {
|
|
|
15
14
|
name: 'storage.folder.created',
|
|
16
15
|
module: 'storage',
|
|
17
16
|
entity: 'folder',
|
|
18
|
-
action:
|
|
17
|
+
action: 'created',
|
|
19
18
|
payload: {
|
|
20
19
|
ids: [
|
|
21
20
|
'1'
|
|
@@ -9,7 +9,6 @@ Object.defineProperty(exports, "MemoryTransport", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _events = require("events");
|
|
12
|
-
const _eventtransportregistry = require("./event-transport.registry");
|
|
13
12
|
function _define_property(obj, key, value) {
|
|
14
13
|
if (key in obj) {
|
|
15
14
|
Object.defineProperty(obj, key, {
|
|
@@ -24,6 +23,7 @@ function _define_property(obj, key, value) {
|
|
|
24
23
|
return obj;
|
|
25
24
|
}
|
|
26
25
|
const CHANNEL = 'domain-event';
|
|
26
|
+
const MAX_LISTENERS = 100;
|
|
27
27
|
let MemoryTransport = class MemoryTransport {
|
|
28
28
|
async connect() {
|
|
29
29
|
this.ready = true;
|
|
@@ -44,11 +44,10 @@ let MemoryTransport = class MemoryTransport {
|
|
|
44
44
|
isReady() {
|
|
45
45
|
return this.ready;
|
|
46
46
|
}
|
|
47
|
-
constructor(
|
|
47
|
+
constructor(){
|
|
48
48
|
_define_property(this, "name", 'memory');
|
|
49
49
|
_define_property(this, "emitter", new _events.EventEmitter());
|
|
50
50
|
_define_property(this, "ready", false);
|
|
51
|
-
this.emitter.setMaxListeners(
|
|
51
|
+
this.emitter.setMaxListeners(MAX_LISTENERS);
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
|
-
_eventtransportregistry.EventTransportRegistry.register('memory', (config)=>new MemoryTransport(config['maxListeners']));
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", {
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
|
-
const _eventactionenum = require("../../enums/event-action.enum");
|
|
6
5
|
const _memorytransport = require("./memory-transport");
|
|
7
6
|
function buildEvent(name = 'storage.folder.created') {
|
|
8
7
|
return {
|
|
@@ -10,7 +9,7 @@ function buildEvent(name = 'storage.folder.created') {
|
|
|
10
9
|
name,
|
|
11
10
|
module: 'storage',
|
|
12
11
|
entity: 'folder',
|
|
13
|
-
action:
|
|
12
|
+
action: 'created',
|
|
14
13
|
payload: {
|
|
15
14
|
ids: [
|
|
16
15
|
'1'
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
const _common = require("@nestjs/common");
|
|
6
|
-
const _eventactionenum = require("../../enums/event-action.enum");
|
|
7
6
|
const _rabbitmqtransportoptional = require("./rabbitmq-transport.optional");
|
|
8
7
|
const mockLoadOptionalModule = jest.fn();
|
|
9
8
|
jest.mock('../../utils/optional-module.util', ()=>({
|
|
@@ -15,7 +14,7 @@ function buildEvent() {
|
|
|
15
14
|
name: 'storage.folder.created',
|
|
16
15
|
module: 'storage',
|
|
17
16
|
entity: 'folder',
|
|
18
|
-
action:
|
|
17
|
+
action: 'created',
|
|
19
18
|
payload: {
|
|
20
19
|
ids: [
|
|
21
20
|
'1'
|
|
@@ -99,14 +99,13 @@ let HybridEventBus = class HybridEventBus {
|
|
|
99
99
|
handler: handler
|
|
100
100
|
});
|
|
101
101
|
if (!await this.ensureConnected()) return;
|
|
102
|
-
await
|
|
103
|
-
this.logger.warn(`Bind failed on ${transport.name}: ${error.message}`);
|
|
104
|
-
})));
|
|
102
|
+
await this.bindSubscriptions();
|
|
105
103
|
}
|
|
106
104
|
async close() {
|
|
107
105
|
await Promise.allSettled(this.transports.map((transport)=>transport.close()));
|
|
108
106
|
this.transports.length = 0;
|
|
109
107
|
this.subscriptions.length = 0;
|
|
108
|
+
this.boundPatterns.clear();
|
|
110
109
|
this.seenIds.clear();
|
|
111
110
|
this.seenOrder.length = 0;
|
|
112
111
|
this.memory = undefined;
|
|
@@ -132,6 +131,8 @@ let HybridEventBus = class HybridEventBus {
|
|
|
132
131
|
return value;
|
|
133
132
|
}
|
|
134
133
|
async buildTransports() {
|
|
134
|
+
// Fresh transports know nothing of what was bound on the ones they replace
|
|
135
|
+
this.boundPatterns.clear();
|
|
135
136
|
if (this.label === 'memory' || this.label === 'hybrid') {
|
|
136
137
|
this.memory = new _memorytransport.MemoryTransport();
|
|
137
138
|
await this.memory.connect();
|
|
@@ -150,6 +151,23 @@ let HybridEventBus = class HybridEventBus {
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
}
|
|
154
|
+
await this.bindSubscriptions();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Binds every pattern the bus knows about, skipping the ones already bound.
|
|
158
|
+
*
|
|
159
|
+
* Called once the transports are built, not only from `subscribe`: a
|
|
160
|
+
* subscription taken while the bus was down returns before any transport
|
|
161
|
+
* exists, so nothing would ever bind its routing key on the broker that
|
|
162
|
+
* finally comes up, leaving the process publishing but never consuming.
|
|
163
|
+
*/ async bindSubscriptions() {
|
|
164
|
+
for (const { pattern } of this.subscriptions){
|
|
165
|
+
if (this.boundPatterns.has(pattern)) continue;
|
|
166
|
+
this.boundPatterns.add(pattern);
|
|
167
|
+
await Promise.all(this.transports.map((transport)=>transport.bind(pattern).catch((error)=>{
|
|
168
|
+
this.logger.warn(`Bind failed on ${transport.name}: ${error.message}`);
|
|
169
|
+
})));
|
|
170
|
+
}
|
|
153
171
|
}
|
|
154
172
|
async createBrokerTransport(label) {
|
|
155
173
|
const name = label === 'hybrid' ? this.resolveHybridBrokerName() : label;
|
|
@@ -241,6 +259,7 @@ let HybridEventBus = class HybridEventBus {
|
|
|
241
259
|
_define_property(this, "logger", void 0);
|
|
242
260
|
_define_property(this, "subscriptions", void 0);
|
|
243
261
|
_define_property(this, "transports", void 0);
|
|
262
|
+
_define_property(this, "boundPatterns", void 0);
|
|
244
263
|
_define_property(this, "seenIds", void 0);
|
|
245
264
|
_define_property(this, "seenOrder", void 0);
|
|
246
265
|
_define_property(this, "label", void 0);
|
|
@@ -252,6 +271,7 @@ let HybridEventBus = class HybridEventBus {
|
|
|
252
271
|
this.logger = new _common.Logger(HybridEventBus.name);
|
|
253
272
|
this.subscriptions = [];
|
|
254
273
|
this.transports = [];
|
|
274
|
+
this.boundPatterns = new Set();
|
|
255
275
|
this.seenIds = new Set();
|
|
256
276
|
this.seenOrder = [];
|
|
257
277
|
this.connected = false;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", {
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
|
-
const _eventactionenum = require("../enums/event-action.enum");
|
|
6
5
|
const _eventtransportregistry = require("./event-transports/event-transport.registry");
|
|
7
6
|
const _memorytransport = require("./event-transports/memory-transport");
|
|
8
7
|
const _hybrideventbusclass = require("./hybrid-event-bus.class");
|
|
@@ -12,7 +11,7 @@ function buildEvent(name = 'storage.folder.created', id = 'event-1') {
|
|
|
12
11
|
name,
|
|
13
12
|
module: 'storage',
|
|
14
13
|
entity: 'folder',
|
|
15
|
-
action:
|
|
14
|
+
action: 'created',
|
|
16
15
|
payload: {
|
|
17
16
|
ids: [
|
|
18
17
|
'1'
|
|
@@ -178,6 +177,36 @@ describe('HybridEventBus', ()=>{
|
|
|
178
177
|
expect(handler).toHaveBeenCalledTimes(1);
|
|
179
178
|
await bus.close();
|
|
180
179
|
});
|
|
180
|
+
it('should bind a subscription taken while disconnected on the broker that comes up later', async ()=>{
|
|
181
|
+
const broker = createFakeTransport('rabbitmq');
|
|
182
|
+
_eventtransportregistry.EventTransportRegistry.register('rabbitmq', ()=>broker);
|
|
183
|
+
const connectSpy = jest.spyOn(_memorytransport.MemoryTransport.prototype, 'connect').mockRejectedValueOnce(new Error('boot failure'));
|
|
184
|
+
const bus = new _hybrideventbusclass.HybridEventBus({
|
|
185
|
+
transport: 'hybrid'
|
|
186
|
+
});
|
|
187
|
+
await bus.subscribe('storage.#', jest.fn());
|
|
188
|
+
expect(broker.bind).not.toHaveBeenCalled();
|
|
189
|
+
connectSpy.mockRestore();
|
|
190
|
+
// Anything that reconnects must bind what is already subscribed, or the
|
|
191
|
+
// queue stays unbound and this process publishes without ever consuming
|
|
192
|
+
await bus.publish(buildEvent());
|
|
193
|
+
expect(broker.bind).toHaveBeenCalledWith('storage.#');
|
|
194
|
+
await bus.close();
|
|
195
|
+
});
|
|
196
|
+
it('should bind each pattern once, not again on every later subscribe', async ()=>{
|
|
197
|
+
const broker = createFakeTransport('rabbitmq');
|
|
198
|
+
_eventtransportregistry.EventTransportRegistry.register('rabbitmq', ()=>broker);
|
|
199
|
+
const bus = new _hybrideventbusclass.HybridEventBus({
|
|
200
|
+
transport: 'rabbitmq'
|
|
201
|
+
});
|
|
202
|
+
await bus.subscribe('storage.#', jest.fn());
|
|
203
|
+
await bus.subscribe('auth.#', jest.fn());
|
|
204
|
+
expect(broker.bind.mock.calls.map(([pattern])=>pattern)).toEqual([
|
|
205
|
+
'storage.#',
|
|
206
|
+
'auth.#'
|
|
207
|
+
]);
|
|
208
|
+
await bus.close();
|
|
209
|
+
});
|
|
181
210
|
it('should forget its transports after close', async ()=>{
|
|
182
211
|
const bus = new _hybrideventbusclass.HybridEventBus({
|
|
183
212
|
transport: 'memory'
|
package/cjs/enums/index.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", {
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
|
-
_export_star(require("./event-action.enum"), exports);
|
|
6
5
|
_export_star(require("./notification-type.enum"), exports);
|
|
7
6
|
_export_star(require("./participant-status.enum"), exports);
|
|
8
7
|
_export_star(require("./recurrence-type.enum"), exports);
|
|
@@ -8,7 +8,6 @@ const _domaineventpublisher = require("../../classes/domain-event.publisher");
|
|
|
8
8
|
const _eventbusregistry = require("../../classes/event-bus.registry");
|
|
9
9
|
const _constants = require("../../constants");
|
|
10
10
|
const _ondomaineventdecorator = require("../../decorators/on-domain-event.decorator");
|
|
11
|
-
const _eventactionenum = require("../../enums/event-action.enum");
|
|
12
11
|
const _interfaces = require("../../interfaces");
|
|
13
12
|
const _eventbusmodule = require("./event-bus.module");
|
|
14
13
|
const _moduleeventsprovider = require("./module-events.provider");
|
|
@@ -156,7 +155,7 @@ describe('EventBusModule (integration)', ()=>{
|
|
|
156
155
|
await (0, _domaineventpublisher.publishDomainEvent)({
|
|
157
156
|
module: 'storage',
|
|
158
157
|
entity: 'folder',
|
|
159
|
-
action:
|
|
158
|
+
action: 'created',
|
|
160
159
|
ids: [
|
|
161
160
|
'folder-1'
|
|
162
161
|
]
|
|
@@ -37,9 +37,14 @@ const SENSITIVE_KEYS = [
|
|
|
37
37
|
/**
|
|
38
38
|
* Matched whole, not as a substring - `pass` must be redacted without also
|
|
39
39
|
* redacting `bypassCache`, and a storage row's `key` is a path, not a secret.
|
|
40
|
+
*
|
|
41
|
+
* `config` is the provider settings blob on the storage and email config rows:
|
|
42
|
+
* S3, Azure, SFTP and SMTP credentials live in it, so it never leaves the
|
|
43
|
+
* process, even though the row itself still publishes.
|
|
40
44
|
*/ const SENSITIVE_EXACT_KEYS = [
|
|
41
45
|
'pass',
|
|
42
|
-
'pwd'
|
|
46
|
+
'pwd',
|
|
47
|
+
'config'
|
|
43
48
|
];
|
|
44
49
|
const REDACTED = '[REDACTED]';
|
|
45
50
|
const CIRCULAR = '[CIRCULAR]';
|
|
@@ -61,10 +61,28 @@ describe('sanitizeEventPayload', ()=>{
|
|
|
61
61
|
apiKey: '[REDACTED]'
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
|
-
it('should redact the
|
|
64
|
+
it('should redact the whole provider settings blob of a storage or email config row', ()=>{
|
|
65
65
|
const result = (0, _eventpatternutil.sanitizeEventPayload)({
|
|
66
66
|
name: 'primary-s3',
|
|
67
|
+
provider: 's3',
|
|
68
|
+
isActive: true,
|
|
67
69
|
config: {
|
|
70
|
+
region: 'us-east-1',
|
|
71
|
+
bucket: 'assets',
|
|
72
|
+
accessKeyId: 'AKIA...',
|
|
73
|
+
secretAccessKey: 's3cr3t'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
expect(result).toEqual({
|
|
77
|
+
name: 'primary-s3',
|
|
78
|
+
provider: 's3',
|
|
79
|
+
isActive: true,
|
|
80
|
+
config: '[REDACTED]'
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
it('should redact the credential keys used by the storage and email providers', ()=>{
|
|
84
|
+
const result = (0, _eventpatternutil.sanitizeEventPayload)({
|
|
85
|
+
settings: {
|
|
68
86
|
region: 'us-east-1',
|
|
69
87
|
bucket: 'assets',
|
|
70
88
|
accessKeyId: 'AKIA...',
|
|
@@ -79,8 +97,7 @@ describe('sanitizeEventPayload', ()=>{
|
|
|
79
97
|
}
|
|
80
98
|
});
|
|
81
99
|
expect(result).toEqual({
|
|
82
|
-
|
|
83
|
-
config: {
|
|
100
|
+
settings: {
|
|
84
101
|
region: 'us-east-1',
|
|
85
102
|
bucket: 'assets',
|
|
86
103
|
accessKeyId: '[REDACTED]',
|