@flusys/nestjs-shared 6.4.0 → 7.0.0-beta.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 (111) hide show
  1. package/README.md +437 -0
  2. package/cjs/classes/api-service.class.js +71 -8
  3. package/cjs/classes/api-service.class.spec.js +220 -0
  4. package/cjs/classes/disabled-event-bus.class.js +15 -0
  5. package/cjs/classes/disabled-event-bus.class.spec.js +40 -0
  6. package/cjs/classes/domain-event.publisher.js +76 -0
  7. package/cjs/classes/domain-event.publisher.spec.js +234 -0
  8. package/cjs/classes/event-bus.registry.js +69 -0
  9. package/cjs/classes/event-bus.registry.spec.js +116 -0
  10. package/cjs/classes/event-transports/event-transport.registry.js +41 -0
  11. package/cjs/classes/event-transports/event-transport.registry.spec.js +31 -0
  12. package/cjs/classes/event-transports/index.js +23 -0
  13. package/cjs/classes/event-transports/kafka-transport.optional.js +151 -0
  14. package/cjs/classes/event-transports/kafka-transport.optional.spec.js +199 -0
  15. package/cjs/classes/event-transports/memory-transport.js +54 -0
  16. package/cjs/classes/event-transports/memory-transport.spec.js +58 -0
  17. package/cjs/classes/event-transports/rabbitmq-transport.optional.js +184 -0
  18. package/cjs/classes/event-transports/rabbitmq-transport.optional.spec.js +256 -0
  19. package/cjs/classes/hybrid-event-bus.class.js +261 -0
  20. package/cjs/classes/hybrid-event-bus.class.spec.js +189 -0
  21. package/cjs/classes/index.js +5 -0
  22. package/cjs/constants/index.js +24 -0
  23. package/cjs/decorators/index.js +1 -0
  24. package/cjs/decorators/on-domain-event.decorator.js +13 -0
  25. package/cjs/decorators/on-domain-event.decorator.spec.js +50 -0
  26. package/cjs/entities/user-root.js +2 -2
  27. package/cjs/enums/event-action.enum.js +18 -0
  28. package/cjs/enums/index.js +1 -0
  29. package/cjs/interfaces/event-bus.interface.js +4 -0
  30. package/cjs/interfaces/index.js +1 -0
  31. package/cjs/modules/event-bus/event-bus.explorer.js +124 -0
  32. package/cjs/modules/event-bus/event-bus.explorer.spec.js +189 -0
  33. package/cjs/modules/event-bus/event-bus.integration.spec.js +254 -0
  34. package/cjs/modules/event-bus/event-bus.module.js +94 -0
  35. package/cjs/modules/event-bus/event-bus.module.spec.js +149 -0
  36. package/cjs/modules/event-bus/module-events.provider.js +39 -0
  37. package/cjs/modules/event-bus/module-events.provider.spec.js +56 -0
  38. package/cjs/modules/index.js +3 -0
  39. package/cjs/utils/event-pattern.util.js +98 -0
  40. package/cjs/utils/event-pattern.util.spec.js +234 -0
  41. package/cjs/utils/index.js +2 -0
  42. package/cjs/utils/optional-module.util.js +35 -0
  43. package/cjs/utils/optional-module.util.spec.js +14 -0
  44. package/classes/api-service.class.d.ts +11 -1
  45. package/classes/disabled-event-bus.class.d.ts +6 -0
  46. package/classes/domain-event.publisher.d.ts +14 -0
  47. package/classes/event-bus.registry.d.ts +14 -0
  48. package/classes/event-transports/event-transport.registry.d.ts +10 -0
  49. package/classes/event-transports/index.d.ts +2 -0
  50. package/classes/event-transports/kafka-transport.optional.d.ts +21 -0
  51. package/classes/event-transports/memory-transport.d.ts +13 -0
  52. package/classes/event-transports/rabbitmq-transport.optional.d.ts +27 -0
  53. package/classes/hybrid-event-bus.class.d.ts +32 -0
  54. package/classes/index.d.ts +5 -0
  55. package/constants/index.d.ts +6 -0
  56. package/decorators/index.d.ts +1 -0
  57. package/decorators/on-domain-event.decorator.d.ts +1 -0
  58. package/enums/event-action.enum.d.ts +7 -0
  59. package/enums/index.d.ts +1 -0
  60. package/fesm/classes/api-service.class.js +71 -8
  61. package/fesm/classes/api-service.class.spec.js +220 -0
  62. package/fesm/classes/disabled-event-bus.class.js +11 -0
  63. package/fesm/classes/disabled-event-bus.class.spec.js +36 -0
  64. package/fesm/classes/domain-event.publisher.js +63 -0
  65. package/fesm/classes/domain-event.publisher.spec.js +230 -0
  66. package/fesm/classes/event-bus.registry.js +65 -0
  67. package/fesm/classes/event-bus.registry.spec.js +112 -0
  68. package/fesm/classes/event-transports/event-transport.registry.js +35 -0
  69. package/fesm/classes/event-transports/event-transport.registry.spec.js +27 -0
  70. package/fesm/classes/event-transports/index.js +6 -0
  71. package/fesm/classes/event-transports/kafka-transport.optional.js +144 -0
  72. package/fesm/classes/event-transports/kafka-transport.optional.spec.js +195 -0
  73. package/fesm/classes/event-transports/memory-transport.js +46 -0
  74. package/fesm/classes/event-transports/memory-transport.spec.js +54 -0
  75. package/fesm/classes/event-transports/rabbitmq-transport.optional.js +174 -0
  76. package/fesm/classes/event-transports/rabbitmq-transport.optional.spec.js +252 -0
  77. package/fesm/classes/hybrid-event-bus.class.js +217 -0
  78. package/fesm/classes/hybrid-event-bus.class.spec.js +185 -0
  79. package/fesm/classes/index.js +5 -0
  80. package/fesm/constants/index.js +7 -0
  81. package/fesm/decorators/index.js +1 -0
  82. package/fesm/decorators/on-domain-event.decorator.js +10 -0
  83. package/fesm/decorators/on-domain-event.decorator.spec.js +46 -0
  84. package/fesm/entities/user-root.js +1 -1
  85. package/fesm/enums/event-action.enum.js +8 -0
  86. package/fesm/enums/index.js +1 -0
  87. package/fesm/interfaces/event-bus.interface.js +1 -0
  88. package/fesm/interfaces/index.js +1 -0
  89. package/fesm/modules/event-bus/event-bus.explorer.js +114 -0
  90. package/fesm/modules/event-bus/event-bus.explorer.spec.js +185 -0
  91. package/fesm/modules/event-bus/event-bus.integration.spec.js +250 -0
  92. package/fesm/modules/event-bus/event-bus.module.js +84 -0
  93. package/fesm/modules/event-bus/event-bus.module.spec.js +145 -0
  94. package/fesm/modules/event-bus/module-events.provider.js +26 -0
  95. package/fesm/modules/event-bus/module-events.provider.spec.js +52 -0
  96. package/fesm/modules/index.js +3 -0
  97. package/fesm/utils/event-pattern.util.js +86 -0
  98. package/fesm/utils/event-pattern.util.spec.js +230 -0
  99. package/fesm/utils/index.js +2 -0
  100. package/fesm/utils/optional-module.util.js +25 -0
  101. package/fesm/utils/optional-module.util.spec.js +10 -0
  102. package/interfaces/event-bus.interface.d.ts +74 -0
  103. package/interfaces/index.d.ts +1 -0
  104. package/modules/event-bus/event-bus.explorer.d.ts +14 -0
  105. package/modules/event-bus/event-bus.module.d.ts +8 -0
  106. package/modules/event-bus/module-events.provider.d.ts +3 -0
  107. package/modules/index.d.ts +3 -0
  108. package/package.json +2 -2
  109. package/utils/event-pattern.util.d.ts +3 -0
  110. package/utils/index.d.ts +2 -0
  111. 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);