@flusys/nestjs-shared 7.0.0 → 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.
Files changed (40) hide show
  1. package/README.md +132 -84
  2. package/cjs/classes/api-service.class.js +37 -47
  3. package/cjs/classes/api-service.class.spec.js +120 -12
  4. package/cjs/classes/event-bus.registry.js +2 -3
  5. package/cjs/classes/event-bus.registry.spec.js +49 -24
  6. package/cjs/classes/event-transports/kafka-transport.optional.spec.js +1 -2
  7. package/cjs/classes/event-transports/memory-transport.js +3 -4
  8. package/cjs/classes/event-transports/memory-transport.spec.js +1 -2
  9. package/cjs/classes/event-transports/rabbitmq-transport.optional.spec.js +1 -2
  10. package/cjs/classes/hybrid-event-bus.class.js +23 -3
  11. package/cjs/classes/hybrid-event-bus.class.spec.js +31 -2
  12. package/cjs/enums/index.js +0 -1
  13. package/cjs/modules/event-bus/event-bus.integration.spec.js +1 -2
  14. package/cjs/utils/event-pattern.util.js +6 -1
  15. package/cjs/utils/event-pattern.util.spec.js +20 -3
  16. package/classes/api-service.class.d.ts +8 -12
  17. package/classes/domain-event.publisher.d.ts +2 -2
  18. package/classes/event-bus.registry.d.ts +2 -2
  19. package/classes/event-transports/memory-transport.d.ts +1 -1
  20. package/classes/hybrid-event-bus.class.d.ts +2 -0
  21. package/enums/index.d.ts +0 -1
  22. package/fesm/classes/api-service.class.js +37 -47
  23. package/fesm/classes/api-service.class.spec.js +120 -12
  24. package/fesm/classes/event-bus.registry.js +2 -3
  25. package/fesm/classes/event-bus.registry.spec.js +49 -24
  26. package/fesm/classes/event-transports/kafka-transport.optional.spec.js +1 -2
  27. package/fesm/classes/event-transports/memory-transport.js +7 -4
  28. package/fesm/classes/event-transports/memory-transport.spec.js +1 -2
  29. package/fesm/classes/event-transports/rabbitmq-transport.optional.spec.js +1 -2
  30. package/fesm/classes/hybrid-event-bus.class.js +23 -3
  31. package/fesm/classes/hybrid-event-bus.class.spec.js +31 -2
  32. package/fesm/enums/index.js +0 -1
  33. package/fesm/modules/event-bus/event-bus.integration.spec.js +1 -2
  34. package/fesm/utils/event-pattern.util.js +6 -1
  35. package/fesm/utils/event-pattern.util.spec.js +20 -3
  36. package/interfaces/event-bus.interface.d.ts +3 -6
  37. package/package.json +2 -2
  38. package/cjs/enums/event-action.enum.js +0 -18
  39. package/enums/event-action.enum.d.ts +0 -7
  40. 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, undefined, TestEntity, dataSourceProvider);
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, undefined, TestEntity, dataSourceProvider);
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, TestEntity, dataSourceProvider);
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: _eventactionenum.EventAction.CREATED,
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: _eventactionenum.EventAction.UPDATED
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
- _eventactionenum.EventAction.CREATED,
914
- _eventactionenum.EventAction.UPDATED
988
+ 'created',
989
+ 'updated'
915
990
  ]);
916
991
  });
917
992
  it.each([
918
993
  [
919
994
  'delete',
920
- _eventactionenum.EventAction.DELETED
995
+ 'deleted'
921
996
  ],
922
997
  [
923
998
  'restore',
924
- _eventactionenum.EventAction.RESTORED
999
+ 'restored'
925
1000
  ],
926
1001
  [
927
1002
  'permanent',
928
- _eventactionenum.EventAction.PURGED
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
- _eventactionenum.EventAction.DELETED
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', _eventactionenum.EventAction.CREATED)).toBe(true);
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', _eventactionenum.EventAction.CREATED)).toBe(false);
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', _eventactionenum.EventAction.CREATED)).toBe(true);
39
- expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder', _eventactionenum.EventAction.DELETED)).toBe(true);
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
- _eventactionenum.EventAction.CREATED
44
+ 'created'
46
45
  ]
47
46
  });
48
- expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder', _eventactionenum.EventAction.CREATED)).toBe(true);
49
- expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder', _eventactionenum.EventAction.UPDATED)).toBe(false);
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
- _eventactionenum.EventAction.CREATED,
54
+ 'created',
56
55
  'logged-in'
57
56
  ]
58
57
  });
59
- expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'appuser', _eventactionenum.EventAction.CREATED)).toBe(true);
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', _eventactionenum.EventAction.UPDATED)).toBe(false);
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', _eventactionenum.EventAction.CREATED)).toBe(false);
71
+ expect(_eventbusregistry.EventBusRegistry.isEnabled('auth', 'appuser', 'created')).toBe(false);
73
72
  });
74
- it('should honour an entity allowlist and exclusion list', ()=>{
75
- _eventbusregistry.EventBusRegistry.configureModule('storage', {
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
- entities: [
78
- 'folder',
79
- 'file_manager'
80
- ],
81
- excludeEntities: [
82
- 'file_manager'
88
+ actions: [
89
+ 'role.*',
90
+ '*.purged'
83
91
  ]
84
92
  });
85
- expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'folder', _eventactionenum.EventAction.CREATED)).toBe(true);
86
- expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'file_manager', _eventactionenum.EventAction.CREATED)).toBe(false);
87
- expect(_eventbusregistry.EventBusRegistry.isEnabled('storage', 'storageConfig', _eventactionenum.EventAction.CREATED)).toBe(false);
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', _eventactionenum.EventAction.CREATED)).toBe(true);
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: _eventactionenum.EventAction.CREATED,
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(maxListeners = 100){
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(maxListeners);
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: _eventactionenum.EventAction.CREATED,
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: _eventactionenum.EventAction.CREATED,
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 Promise.all(this.transports.map((transport)=>transport.bind(pattern).catch((error)=>{
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: _eventactionenum.EventAction.CREATED,
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'
@@ -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: _eventactionenum.EventAction.CREATED,
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 credential keys used by the storage and email providers', ()=>{
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
- name: 'primary-s3',
83
- config: {
100
+ settings: {
84
101
  region: 'us-east-1',
85
102
  bucket: 'assets',
86
103
  accessKeyId: '[REDACTED]',