@flusys/nestjs-event-manager 7.1.0 → 8.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.
Files changed (103) hide show
  1. package/adapters/event-manager.adapter.d.ts +2 -2
  2. package/adapters/index.d.ts +1 -1
  3. package/config/index.d.ts +3 -3
  4. package/controllers/event.controller.d.ts +3 -3
  5. package/controllers/index.d.ts +1 -1
  6. package/docs/event-manager-swagger.config.d.ts +2 -2
  7. package/docs/index.d.ts +1 -1
  8. package/dtos/calendar-query.dto.d.ts +1 -1
  9. package/dtos/event.dto.d.ts +1 -1
  10. package/dtos/index.d.ts +3 -3
  11. package/entities/event-with-company.entity.d.ts +1 -1
  12. package/entities/index.d.ts +8 -8
  13. package/fesm/477.js +1798 -0
  14. package/fesm/833.js +840 -0
  15. package/fesm/adapters/index.js +884 -1
  16. package/fesm/config/index.js +87 -3
  17. package/fesm/controllers/index.js +365 -1
  18. package/fesm/docs/index.js +155 -2
  19. package/fesm/dtos/index.js +720 -3
  20. package/fesm/entities/index.js +350 -11
  21. package/fesm/index.js +121 -9
  22. package/fesm/interfaces/index.js +16 -2
  23. package/fesm/modules/index.js +13 -1
  24. package/fesm/runtime.js +85 -0
  25. package/fesm/services/index.js +272 -4
  26. package/index.d.ts +9 -9
  27. package/interfaces/event-manager-module.interface.d.ts +3 -3
  28. package/interfaces/event.interface.d.ts +1 -1
  29. package/interfaces/index.d.ts +2 -2
  30. package/modules/event-manager.module.d.ts +2 -2
  31. package/modules/index.d.ts +1 -1
  32. package/package.json +29 -39
  33. package/services/event-manager-config.service.d.ts +3 -3
  34. package/services/event-manager-datasource.provider.d.ts +3 -3
  35. package/services/event-manager-helper.service.d.ts +3 -3
  36. package/services/event.service.d.ts +7 -7
  37. package/services/index.d.ts +4 -4
  38. package/cjs/adapters/event-manager.adapter.js +0 -65
  39. package/cjs/adapters/event-manager.adapter.spec.js +0 -104
  40. package/cjs/adapters/index.js +0 -18
  41. package/cjs/config/event-actions.js +0 -31
  42. package/cjs/config/event-manager.constants.js +0 -13
  43. package/cjs/config/index.js +0 -20
  44. package/cjs/config/message-keys.js +0 -27
  45. package/cjs/controllers/event.controller.js +0 -190
  46. package/cjs/controllers/event.controller.spec.js +0 -156
  47. package/cjs/controllers/index.js +0 -18
  48. package/cjs/docs/event-manager-swagger.config.js +0 -125
  49. package/cjs/docs/event-manager-swagger.config.spec.js +0 -65
  50. package/cjs/docs/index.js +0 -21
  51. package/cjs/dtos/calendar-query.dto.js +0 -212
  52. package/cjs/dtos/event-participant.dto.js +0 -181
  53. package/cjs/dtos/event.dto.js +0 -343
  54. package/cjs/dtos/index.js +0 -20
  55. package/cjs/entities/event-participant.entity.js +0 -89
  56. package/cjs/entities/event-with-company.entity.js +0 -56
  57. package/cjs/entities/event.entity.js +0 -156
  58. package/cjs/entities/index.js +0 -55
  59. package/cjs/entities/index.spec.js +0 -18
  60. package/cjs/index.js +0 -27
  61. package/cjs/interfaces/event-manager-module.interface.js +0 -4
  62. package/cjs/interfaces/event.interface.js +0 -4
  63. package/cjs/interfaces/index.js +0 -19
  64. package/cjs/modules/event-manager.module.js +0 -132
  65. package/cjs/modules/event-manager.module.spec.js +0 -146
  66. package/cjs/modules/index.js +0 -18
  67. package/cjs/services/event-manager-config.service.js +0 -76
  68. package/cjs/services/event-manager-config.service.spec.js +0 -111
  69. package/cjs/services/event-manager-datasource.provider.js +0 -84
  70. package/cjs/services/event-manager-datasource.provider.spec.js +0 -198
  71. package/cjs/services/event-manager-helper.service.js +0 -235
  72. package/cjs/services/event-manager-helper.service.spec.js +0 -327
  73. package/cjs/services/event.service.js +0 -481
  74. package/cjs/services/event.service.spec.js +0 -929
  75. package/cjs/services/index.js +0 -21
  76. package/fesm/adapters/event-manager.adapter.js +0 -55
  77. package/fesm/adapters/event-manager.adapter.spec.js +0 -100
  78. package/fesm/config/event-actions.js +0 -10
  79. package/fesm/config/event-manager.constants.js +0 -3
  80. package/fesm/config/message-keys.js +0 -9
  81. package/fesm/controllers/event.controller.js +0 -180
  82. package/fesm/controllers/event.controller.spec.js +0 -152
  83. package/fesm/docs/event-manager-swagger.config.js +0 -118
  84. package/fesm/docs/event-manager-swagger.config.spec.js +0 -61
  85. package/fesm/dtos/calendar-query.dto.js +0 -194
  86. package/fesm/dtos/event-participant.dto.js +0 -157
  87. package/fesm/dtos/event.dto.js +0 -322
  88. package/fesm/entities/event-participant.entity.js +0 -79
  89. package/fesm/entities/event-with-company.entity.js +0 -46
  90. package/fesm/entities/event.entity.js +0 -146
  91. package/fesm/entities/index.spec.js +0 -14
  92. package/fesm/interfaces/event-manager-module.interface.js +0 -3
  93. package/fesm/interfaces/event.interface.js +0 -1
  94. package/fesm/modules/event-manager.module.js +0 -122
  95. package/fesm/modules/event-manager.module.spec.js +0 -142
  96. package/fesm/services/event-manager-config.service.js +0 -66
  97. package/fesm/services/event-manager-config.service.spec.js +0 -107
  98. package/fesm/services/event-manager-datasource.provider.js +0 -74
  99. package/fesm/services/event-manager-datasource.provider.spec.js +0 -194
  100. package/fesm/services/event-manager-helper.service.js +0 -225
  101. package/fesm/services/event-manager-helper.service.spec.js +0 -323
  102. package/fesm/services/event.service.js +0 -471
  103. package/fesm/services/event.service.spec.js +0 -925
@@ -1,929 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- const _common = require("@nestjs/common");
6
- const _classes = require("@flusys/nestjs-shared/classes");
7
- const _enums = require("@flusys/nestjs-shared/enums");
8
- const _repositorymock = require("@test-utils/mocks/repository.mock");
9
- const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
10
- const _config = require("../config");
11
- const _entities = require("../entities");
12
- const _eventservice = require("./event.service");
13
- function buildEvent(overrides = {}) {
14
- return {
15
- id: 'event-uuid-1',
16
- title: 'Sprint Planning',
17
- description: null,
18
- eventDate: '2026-04-03',
19
- startTime: '09:00',
20
- endTime: '10:00',
21
- isAllDay: false,
22
- recurrenceType: _enums.RecurrenceType.NONE,
23
- recurrenceEndDate: null,
24
- weekDays: null,
25
- color: '#3B82F6',
26
- meetingLink: null,
27
- isActive: true,
28
- createdAt: new Date('2026-01-01T00:00:00Z'),
29
- updatedAt: new Date('2026-01-01T00:00:00Z'),
30
- deletedAt: null,
31
- createdById: null,
32
- updatedById: null,
33
- deletedById: null,
34
- ...overrides
35
- };
36
- }
37
- function buildParticipant(overrides = {}) {
38
- return {
39
- id: 'participant-uuid-1',
40
- eventId: 'event-uuid-1',
41
- userId: 'user-uuid-1',
42
- status: _enums.ParticipantStatus.PENDING,
43
- isOrganizer: false,
44
- createdAt: new Date('2026-01-01T00:00:00Z'),
45
- updatedAt: new Date('2026-01-01T00:00:00Z'),
46
- deletedAt: null,
47
- createdById: null,
48
- updatedById: null,
49
- deletedById: null,
50
- ...overrides
51
- };
52
- }
53
- describe('EventService', ()=>{
54
- const mockUtilsService = {
55
- clearCache: jest.fn(),
56
- getCacheKey: jest.fn().mockReturnValue('cache-key'),
57
- trackCacheKey: jest.fn()
58
- };
59
- // Call history is shared across tests in this file; reset it so each cache assertion is exact.
60
- beforeEach(()=>{
61
- mockUtilsService.clearCache.mockClear();
62
- });
63
- let service;
64
- let mockEventRepo;
65
- let mockParticipantRepo;
66
- let mockConfigService;
67
- let mockDataSourceProvider;
68
- let mockEventQb;
69
- const user = (0, _loggedusermock.buildMockUser)();
70
- beforeEach(()=>{
71
- mockEventRepo = (0, _repositorymock.createMockRepository)();
72
- mockParticipantRepo = (0, _repositorymock.createMockRepository)();
73
- mockEventQb = mockEventRepo.createQueryBuilder();
74
- // `leftJoinAndMapMany` isn't part of the shared MockQueryBuilder.
75
- mockEventQb.leftJoinAndMapMany = jest.fn().mockReturnThis();
76
- mockConfigService = {
77
- isCompanyFeatureEnabled: jest.fn().mockReturnValue(false),
78
- getDefaultColor: jest.fn().mockReturnValue('#3B82F6'),
79
- getMaxRecurrenceOccurrences: jest.fn().mockReturnValue(365)
80
- };
81
- mockDataSourceProvider = {
82
- getRepository: jest.fn((entity)=>Promise.resolve(entity === _entities.EventParticipant ? mockParticipantRepo : mockEventRepo)),
83
- getDataSource: jest.fn()
84
- };
85
- service = new _eventservice.EventService({}, mockUtilsService, mockConfigService, mockDataSourceProvider);
86
- });
87
- describe('entity selection', ()=>{
88
- function buildWithCompanyFeature(enabled) {
89
- mockConfigService.isCompanyFeatureEnabled.mockReturnValue(enabled);
90
- return new _eventservice.EventService({}, mockUtilsService, mockConfigService, mockDataSourceProvider);
91
- }
92
- it('uses the company-scoped entity when the company feature is enabled', ()=>{
93
- expect(buildWithCompanyFeature(true)['_entity']).toBe(_entities.EventWithCompany);
94
- });
95
- it('uses the core entity when the company feature is disabled', ()=>{
96
- expect(buildWithCompanyFeature(false)['_entity']).toBe(_entities.Event);
97
- });
98
- });
99
- describe('convertSingleDtoToEntity', ()=>{
100
- it('builds a new entity with sensible defaults on create', async ()=>{
101
- const dto = {
102
- title: 'Sprint Planning',
103
- eventDate: '2026-04-03',
104
- startTime: '09:00',
105
- endTime: '10:00'
106
- };
107
- const entity = await service.convertSingleDtoToEntity(dto, user);
108
- expect(entity).toMatchObject({
109
- title: 'Sprint Planning',
110
- startTime: '09:00',
111
- endTime: '10:00',
112
- isAllDay: false,
113
- recurrenceType: _enums.RecurrenceType.NONE,
114
- recurrenceEndDate: null,
115
- color: '#3B82F6',
116
- isActive: true
117
- });
118
- });
119
- it('forces 00:00 / 23:59 markers on create for all-day events', async ()=>{
120
- const dto = {
121
- title: 'Holiday',
122
- eventDate: '2026-12-25',
123
- startTime: '09:00',
124
- endTime: '17:00',
125
- isAllDay: true
126
- };
127
- const entity = await service.convertSingleDtoToEntity(dto, user);
128
- expect(entity.startTime).toBe('00:00');
129
- expect(entity.endTime).toBe('23:59');
130
- });
131
- it('attaches companyId from the current user when the company feature is enabled', async ()=>{
132
- mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
133
- const dto = {
134
- title: 'Sprint Planning',
135
- eventDate: '2026-04-03',
136
- startTime: '09:00',
137
- endTime: '10:00'
138
- };
139
- const entity = await service.convertSingleDtoToEntity(dto, user);
140
- expect(entity.companyId).toBe(user.companyId);
141
- });
142
- it('queues participantIds for the after-insert hook when supplied', async ()=>{
143
- const dto = {
144
- title: 'Sprint Planning',
145
- eventDate: '2026-04-03',
146
- startTime: '09:00',
147
- endTime: '10:00',
148
- participantIds: [
149
- 'participant-uuid-1'
150
- ]
151
- };
152
- await service.convertSingleDtoToEntity(dto, user);
153
- expect(service.pendingParticipantsList).toEqual([
154
- [
155
- 'participant-uuid-1'
156
- ]
157
- ]);
158
- });
159
- it('merges an update dto onto the existing entity, preserving untouched fields', async ()=>{
160
- service.repository = mockEventRepo;
161
- const existing = buildEvent({
162
- title: 'Original Title',
163
- color: '#111111'
164
- });
165
- mockEventRepo.findOne.mockResolvedValue(existing);
166
- const dto = {
167
- id: 'event-uuid-1',
168
- title: 'Updated Title'
169
- };
170
- const entity = await service.convertSingleDtoToEntity(dto, user);
171
- expect(entity.title).toBe('Updated Title');
172
- expect(entity.color).toBe('#111111');
173
- });
174
- it('throws NotFoundException with the correct messageKey when updating a missing event', async ()=>{
175
- service.repository = mockEventRepo;
176
- mockEventRepo.findOne.mockResolvedValue(null);
177
- const dto = {
178
- id: 'missing-uuid',
179
- title: 'Updated Title'
180
- };
181
- await expect(service.convertSingleDtoToEntity(dto, user)).rejects.toMatchObject({
182
- response: expect.objectContaining({
183
- messageKey: _config.EVENT_MESSAGES.NOT_FOUND
184
- })
185
- });
186
- });
187
- it('clears an explicitly-null meetingLink but preserves the existing one when omitted', async ()=>{
188
- service.repository = mockEventRepo;
189
- const existing = buildEvent({
190
- meetingLink: 'https://meet.example.com/original'
191
- });
192
- mockEventRepo.findOne.mockResolvedValue(existing);
193
- const clearedDto = {
194
- id: 'event-uuid-1',
195
- meetingLink: null
196
- };
197
- const clearedEntity = await service.convertSingleDtoToEntity(clearedDto, user);
198
- expect(clearedEntity.meetingLink).toBeNull();
199
- const omittedDto = {
200
- id: 'event-uuid-1'
201
- };
202
- const preservedEntity = await service.convertSingleDtoToEntity(omittedDto, user);
203
- expect(preservedEntity.meetingLink).toBe('https://meet.example.com/original');
204
- });
205
- });
206
- describe('getFilterQuery', ()=>{
207
- it('filters by isActive when provided as a boolean', async ()=>{
208
- const query = mockEventQb;
209
- await service.getFilterQuery(query, {
210
- isActive: true
211
- }, user);
212
- expect(query.andWhere).toHaveBeenCalledWith('event.isActive = :isActive', {
213
- isActive: true
214
- });
215
- });
216
- it('filters by recurrenceType', async ()=>{
217
- const query = mockEventQb;
218
- await service.getFilterQuery(query, {
219
- recurrenceType: _enums.RecurrenceType.WEEKLY
220
- }, user);
221
- expect(query.andWhere).toHaveBeenCalledWith('event.recurrenceType = :recurrenceType', {
222
- recurrenceType: _enums.RecurrenceType.WEEKLY
223
- });
224
- });
225
- it('scopes to participant event ids when participantUserId is provided and matches exist', async ()=>{
226
- const participantQb = mockParticipantRepo.createQueryBuilder();
227
- participantQb.getRawMany = jest.fn().mockResolvedValue([
228
- {
229
- ep_event_id: 'event-uuid-1'
230
- }
231
- ]);
232
- const query = mockEventQb;
233
- await service.getFilterQuery(query, {
234
- participantUserId: 'user-uuid-1'
235
- }, user);
236
- expect(query.andWhere).toHaveBeenCalledWith('event.id IN (:...participantEventIds)', {
237
- participantEventIds: [
238
- 'event-uuid-1'
239
- ]
240
- });
241
- });
242
- it('short-circuits to an always-false predicate when the user participates in no events', async ()=>{
243
- const participantQb = mockParticipantRepo.createQueryBuilder();
244
- participantQb.getRawMany = jest.fn().mockResolvedValue([]);
245
- const query = mockEventQb;
246
- await service.getFilterQuery(query, {
247
- participantUserId: 'user-uuid-1'
248
- }, user);
249
- expect(query.andWhere).toHaveBeenCalledWith('1 = 0');
250
- });
251
- it('falls back to an equality filter for unrecognized keys', async ()=>{
252
- const query = mockEventQb;
253
- await service.getFilterQuery(query, {
254
- color: '#000000'
255
- }, user);
256
- expect(query.andWhere).toHaveBeenCalledWith('event.color = :value', {
257
- value: '#000000'
258
- });
259
- });
260
- });
261
- describe('getExtraManipulateQuery', ()=>{
262
- it('applies the company filter and left-joins participants', async ()=>{
263
- mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
264
- const query = mockEventQb;
265
- await service.getExtraManipulateQuery(query, {}, user);
266
- expect(query.andWhere).toHaveBeenCalledWith('event.companyId = :companyId', {
267
- companyId: user.companyId
268
- });
269
- expect(query.leftJoinAndMapMany).toHaveBeenCalledWith('event.participants', _entities.EventParticipant, 'ep', 'ep.event_id = event.id AND ep.deleted_at IS NULL');
270
- });
271
- it('does not apply a company filter when the company feature is disabled', async ()=>{
272
- mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
273
- const query = mockEventQb;
274
- await service.getExtraManipulateQuery(query, {}, user);
275
- const companyCalls = query.andWhere.mock.calls.filter((call)=>call[0] === 'event.companyId = :companyId');
276
- expect(companyCalls).toHaveLength(0);
277
- });
278
- });
279
- describe('afterInsertOperation / afterUpdateOperation', ()=>{
280
- it('adds the current user and requested participants as organizer + attendees on insert', async ()=>{
281
- mockParticipantRepo.find.mockResolvedValue([]);
282
- mockParticipantRepo.save.mockResolvedValue([]);
283
- service.pendingParticipantsList = [
284
- [
285
- 'participant-uuid-2'
286
- ]
287
- ];
288
- await service.afterInsertOperation([
289
- buildEvent()
290
- ], {}, user, {});
291
- const savedParticipants = mockParticipantRepo.save.mock.calls[0][0];
292
- expect(savedParticipants).toEqual(expect.arrayContaining([
293
- expect.objectContaining({
294
- userId: user.id,
295
- isOrganizer: true
296
- }),
297
- expect.objectContaining({
298
- userId: 'participant-uuid-2',
299
- isOrganizer: false
300
- })
301
- ]));
302
- expect(service.pendingParticipantsList).toEqual([]);
303
- });
304
- it('restores a previously soft-deleted participant instead of duplicating it', async ()=>{
305
- const softDeleted = buildParticipant({
306
- userId: user.id,
307
- deletedAt: new Date()
308
- });
309
- mockParticipantRepo.find.mockResolvedValue([
310
- softDeleted
311
- ]);
312
- mockParticipantRepo.save.mockResolvedValue([]);
313
- service.pendingParticipantsList = [
314
- []
315
- ];
316
- await service.afterInsertOperation([
317
- buildEvent()
318
- ], {}, user, {});
319
- const savedParticipants = mockParticipantRepo.save.mock.calls[0][0];
320
- expect(savedParticipants[0]).toMatchObject({
321
- userId: user.id,
322
- deletedAt: null,
323
- isOrganizer: true
324
- });
325
- });
326
- it('does nothing when there is no authenticated user', async ()=>{
327
- service.pendingParticipantsList = [
328
- [
329
- 'participant-uuid-2'
330
- ]
331
- ];
332
- await service.afterInsertOperation([
333
- buildEvent()
334
- ], {}, null, {});
335
- expect(mockParticipantRepo.save).not.toHaveBeenCalled();
336
- });
337
- it('soft-deletes existing participants before re-adding the current set on update', async ()=>{
338
- mockParticipantRepo.find.mockResolvedValue([]);
339
- mockParticipantRepo.save.mockResolvedValue([]);
340
- mockParticipantRepo.update.mockResolvedValue({});
341
- service.pendingParticipantsList = [
342
- [
343
- 'participant-uuid-2'
344
- ]
345
- ];
346
- await service.afterUpdateOperation([
347
- buildEvent()
348
- ], {}, user, {});
349
- expect(mockParticipantRepo.update).toHaveBeenCalledWith({
350
- eventId: 'event-uuid-1',
351
- deletedAt: expect.anything()
352
- }, {
353
- deletedAt: expect.any(Date)
354
- });
355
- expect(mockParticipantRepo.save).toHaveBeenCalled();
356
- expect(service.pendingParticipantsList).toEqual([]);
357
- });
358
- });
359
- describe('updateParticipantStatus', ()=>{
360
- it('updates the participant status and returns the saved entity', async ()=>{
361
- const participant = buildParticipant({
362
- status: _enums.ParticipantStatus.PENDING
363
- });
364
- mockParticipantRepo.findOne.mockResolvedValue(participant);
365
- mockParticipantRepo.save.mockImplementation(async (p)=>p);
366
- const dto = {
367
- id: 'participant-uuid-1',
368
- status: _enums.ParticipantStatus.TENTATIVE
369
- };
370
- const result = await service.updateParticipantStatus(dto, user);
371
- expect(result.status).toBe(_enums.ParticipantStatus.TENTATIVE);
372
- expect(mockParticipantRepo.save).toHaveBeenCalledWith(expect.objectContaining({
373
- status: _enums.ParticipantStatus.TENTATIVE
374
- }));
375
- });
376
- it('invalidates the event list cache and the parent event entry', async ()=>{
377
- const participant = buildParticipant({
378
- status: _enums.ParticipantStatus.PENDING
379
- });
380
- mockParticipantRepo.findOne.mockResolvedValue(participant);
381
- mockParticipantRepo.save.mockImplementation(async (p)=>p);
382
- await service.updateParticipantStatus({
383
- id: participant.id,
384
- status: _enums.ParticipantStatus.ACCEPTED
385
- }, user);
386
- expect(mockUtilsService.clearCache).toHaveBeenCalledWith('event', expect.anything());
387
- expect(mockUtilsService.clearCache).toHaveBeenCalledWith('event', expect.anything(), participant.eventId);
388
- });
389
- it('throws NotFoundException with the correct messageKey when the participant is missing', async ()=>{
390
- mockParticipantRepo.findOne.mockResolvedValue(null);
391
- const dto = {
392
- id: 'missing-uuid',
393
- status: _enums.ParticipantStatus.ACCEPTED
394
- };
395
- await expect(service.updateParticipantStatus(dto, user)).rejects.toMatchObject({
396
- response: expect.objectContaining({
397
- messageKey: _config.EVENT_PARTICIPANT_MESSAGES.NOT_FOUND
398
- })
399
- });
400
- await expect(service.updateParticipantStatus(dto, user)).rejects.toBeInstanceOf(_common.NotFoundException);
401
- });
402
- });
403
- describe('getEventsForCalendarRange', ()=>{
404
- function queryDto(overrides = {}) {
405
- return {
406
- startDate: '2026-04-01',
407
- endDate: '2026-04-30',
408
- ...overrides
409
- };
410
- }
411
- it('returns a single non-recurring event unchanged when it falls in range', async ()=>{
412
- mockEventQb.getMany.mockResolvedValue([
413
- buildEvent({
414
- eventDate: '2026-04-10'
415
- })
416
- ]);
417
- const result = await service.getEventsForCalendarRange(queryDto(), user);
418
- expect(result).toHaveLength(1);
419
- expect(result[0].id).toBe('event-uuid-1');
420
- expect(result[0].isRecurrenceInstance).toBeUndefined();
421
- });
422
- it('defaults activeOnly to true when not specified', async ()=>{
423
- mockEventQb.getMany.mockResolvedValue([]);
424
- await service.getEventsForCalendarRange(queryDto(), user);
425
- expect(mockEventQb.andWhere).toHaveBeenCalledWith('event.isActive = :isActive', {
426
- isActive: true
427
- });
428
- });
429
- it('respects activeOnly = false', async ()=>{
430
- mockEventQb.getMany.mockResolvedValue([]);
431
- await service.getEventsForCalendarRange(queryDto({
432
- activeOnly: false
433
- }), user);
434
- expect(mockEventQb.andWhere).toHaveBeenCalledWith('event.isActive = :isActive', {
435
- isActive: false
436
- });
437
- });
438
- it('handles a zero-length range (startDate === endDate)', async ()=>{
439
- mockEventQb.getMany.mockResolvedValue([
440
- buildEvent({
441
- eventDate: '2026-04-10'
442
- })
443
- ]);
444
- const result = await service.getEventsForCalendarRange(queryDto({
445
- startDate: '2026-04-10',
446
- endDate: '2026-04-10'
447
- }), user);
448
- expect(mockEventQb.andWhere).toHaveBeenCalledWith(expect.stringContaining('event.eventDate >= :startDate'), {
449
- startDate: '2026-04-10',
450
- endDate: '2026-04-10',
451
- none: _enums.RecurrenceType.NONE
452
- });
453
- expect(result).toHaveLength(1);
454
- });
455
- it('short-circuits to an empty array when filtering by a userId with no participant events', async ()=>{
456
- const participantQb = mockParticipantRepo.createQueryBuilder();
457
- participantQb.getRawMany = jest.fn().mockResolvedValue([]);
458
- const result = await service.getEventsForCalendarRange(queryDto({
459
- userId: 'user-uuid-1'
460
- }), user);
461
- expect(result).toEqual([]);
462
- expect(mockEventQb.getMany).not.toHaveBeenCalled();
463
- });
464
- it('filters events to the participant event ids when userId matches events', async ()=>{
465
- const participantQb = mockParticipantRepo.createQueryBuilder();
466
- participantQb.getRawMany = jest.fn().mockResolvedValue([
467
- {
468
- ep_event_id: 'event-uuid-1'
469
- }
470
- ]);
471
- mockEventQb.getMany.mockResolvedValue([
472
- buildEvent()
473
- ]);
474
- await service.getEventsForCalendarRange(queryDto({
475
- userId: 'user-uuid-1'
476
- }), user);
477
- expect(mockEventQb.andWhere).toHaveBeenCalledWith('event.id IN (:...eventIds)', {
478
- eventIds: [
479
- 'event-uuid-1'
480
- ]
481
- });
482
- });
483
- it('loads and attaches participants when includeParticipants is true', async ()=>{
484
- mockEventQb.getMany.mockResolvedValue([
485
- buildEvent()
486
- ]);
487
- mockParticipantRepo.find.mockResolvedValue([
488
- buildParticipant({
489
- eventId: 'event-uuid-1',
490
- userId: 'user-uuid-1'
491
- }),
492
- buildParticipant({
493
- id: 'participant-uuid-2',
494
- eventId: 'event-uuid-1',
495
- userId: 'user-uuid-2'
496
- })
497
- ]);
498
- const result = await service.getEventsForCalendarRange(queryDto({
499
- includeParticipants: true
500
- }), user);
501
- expect(result[0].participants).toHaveLength(2);
502
- expect(result[0].participants?.[0].userId).toBe('user-uuid-1');
503
- });
504
- it('does not query participants when includeParticipants is falsy', async ()=>{
505
- mockEventQb.getMany.mockResolvedValue([
506
- buildEvent()
507
- ]);
508
- await service.getEventsForCalendarRange(queryDto(), user);
509
- expect(mockParticipantRepo.find).not.toHaveBeenCalled();
510
- });
511
- it('does not query participants when there are no events to attach them to', async ()=>{
512
- mockEventQb.getMany.mockResolvedValue([]);
513
- await service.getEventsForCalendarRange(queryDto({
514
- includeParticipants: true
515
- }), user);
516
- expect(mockParticipantRepo.find).not.toHaveBeenCalled();
517
- });
518
- // ---------------------------------------------------------------------
519
- // Recurrence expansion — daily
520
- // ---------------------------------------------------------------------
521
- it('expands an infinite daily recurrence within the range, marking only the first occurrence as non-instance', async ()=>{
522
- mockEventQb.getMany.mockResolvedValue([
523
- buildEvent({
524
- eventDate: '2026-01-01',
525
- recurrenceType: _enums.RecurrenceType.DAILY
526
- })
527
- ]);
528
- const result = await service.getEventsForCalendarRange(queryDto({
529
- startDate: '2026-01-01',
530
- endDate: '2026-01-05'
531
- }), user);
532
- expect(result.map((e)=>e.eventDate)).toEqual([
533
- '2026-01-01',
534
- '2026-01-02',
535
- '2026-01-03',
536
- '2026-01-04',
537
- '2026-01-05'
538
- ]);
539
- expect(result[0].isRecurrenceInstance).toBe(false);
540
- expect(result.slice(1).every((e)=>e.isRecurrenceInstance === true)).toBe(true);
541
- expect(result.every((e)=>e.originalEventId === 'event-uuid-1')).toBe(true);
542
- expect(result[1].id).toBe('event-uuid-1_2026-01-02');
543
- });
544
- it('stops a daily recurrence at the until (recurrenceEndDate) bound, before the range end', async ()=>{
545
- mockEventQb.getMany.mockResolvedValue([
546
- buildEvent({
547
- eventDate: '2026-01-01',
548
- recurrenceType: _enums.RecurrenceType.DAILY,
549
- recurrenceEndDate: '2026-01-03'
550
- })
551
- ]);
552
- const result = await service.getEventsForCalendarRange(queryDto({
553
- startDate: '2026-01-01',
554
- endDate: '2026-01-10'
555
- }), user);
556
- expect(result.map((e)=>e.eventDate)).toEqual([
557
- '2026-01-01',
558
- '2026-01-02',
559
- '2026-01-03'
560
- ]);
561
- });
562
- it('caps a count-based (no end date) recurrence at the configured max occurrences', async ()=>{
563
- mockConfigService.getMaxRecurrenceOccurrences.mockReturnValue(3);
564
- mockEventQb.getMany.mockResolvedValue([
565
- buildEvent({
566
- eventDate: '2026-01-01',
567
- recurrenceType: _enums.RecurrenceType.DAILY
568
- })
569
- ]);
570
- const result = await service.getEventsForCalendarRange(queryDto({
571
- startDate: '2026-01-01',
572
- endDate: '2026-12-31'
573
- }), user);
574
- expect(result).toHaveLength(3);
575
- expect(result.map((e)=>e.eventDate)).toEqual([
576
- '2026-01-01',
577
- '2026-01-02',
578
- '2026-01-03'
579
- ]);
580
- });
581
- it('returns a single occurrence when the recurrence range only covers the start date', async ()=>{
582
- mockEventQb.getMany.mockResolvedValue([
583
- buildEvent({
584
- eventDate: '2026-01-01',
585
- recurrenceType: _enums.RecurrenceType.DAILY
586
- })
587
- ]);
588
- const result = await service.getEventsForCalendarRange(queryDto({
589
- startDate: '2026-01-01',
590
- endDate: '2026-01-01'
591
- }), user);
592
- expect(result).toHaveLength(1);
593
- expect(result[0].eventDate).toBe('2026-01-01');
594
- });
595
- it('produces no occurrences when the query range is entirely before the series start', async ()=>{
596
- mockEventQb.getMany.mockResolvedValue([
597
- buildEvent({
598
- eventDate: '2026-06-01',
599
- recurrenceType: _enums.RecurrenceType.DAILY,
600
- recurrenceEndDate: '2026-06-10'
601
- })
602
- ]);
603
- const result = await service.getEventsForCalendarRange(queryDto({
604
- startDate: '2026-01-01',
605
- endDate: '2026-01-31'
606
- }), user);
607
- expect(result).toEqual([]);
608
- });
609
- it('does not shift dates across a US daylight-saving boundary (spring-forward, 2026-03-08)', async ()=>{
610
- mockEventQb.getMany.mockResolvedValue([
611
- buildEvent({
612
- eventDate: '2026-03-06',
613
- recurrenceType: _enums.RecurrenceType.DAILY
614
- })
615
- ]);
616
- const result = await service.getEventsForCalendarRange(queryDto({
617
- startDate: '2026-03-06',
618
- endDate: '2026-03-10'
619
- }), user);
620
- expect(result.map((e)=>e.eventDate)).toEqual([
621
- '2026-03-06',
622
- '2026-03-07',
623
- '2026-03-08',
624
- '2026-03-09',
625
- '2026-03-10'
626
- ]);
627
- });
628
- it('does not shift dates across a US daylight-saving boundary (fall-back, 2026-11-01)', async ()=>{
629
- mockEventQb.getMany.mockResolvedValue([
630
- buildEvent({
631
- eventDate: '2026-10-30',
632
- recurrenceType: _enums.RecurrenceType.DAILY
633
- })
634
- ]);
635
- const result = await service.getEventsForCalendarRange(queryDto({
636
- startDate: '2026-10-30',
637
- endDate: '2026-11-03'
638
- }), user);
639
- expect(result.map((e)=>e.eventDate)).toEqual([
640
- '2026-10-30',
641
- '2026-10-31',
642
- '2026-11-01',
643
- '2026-11-02',
644
- '2026-11-03'
645
- ]);
646
- });
647
- // ---------------------------------------------------------------------
648
- // Recurrence expansion — weekly / biweekly
649
- // ---------------------------------------------------------------------
650
- it('expands a weekly recurrence using the event own weekday when weekDays is not set', async ()=>{
651
- // 2026-01-05 is a Monday
652
- mockEventQb.getMany.mockResolvedValue([
653
- buildEvent({
654
- eventDate: '2026-01-05',
655
- recurrenceType: _enums.RecurrenceType.WEEKLY,
656
- weekDays: null
657
- })
658
- ]);
659
- const result = await service.getEventsForCalendarRange(queryDto({
660
- startDate: '2026-01-05',
661
- endDate: '2026-01-26'
662
- }), user);
663
- expect(result.map((e)=>e.eventDate)).toEqual([
664
- '2026-01-05',
665
- '2026-01-12',
666
- '2026-01-19',
667
- '2026-01-26'
668
- ]);
669
- });
670
- it('expands a weekly recurrence across multiple weekDays', async ()=>{
671
- // 2026-01-05 is a Monday; weekDays MON + WED
672
- mockEventQb.getMany.mockResolvedValue([
673
- buildEvent({
674
- eventDate: '2026-01-05',
675
- recurrenceType: _enums.RecurrenceType.WEEKLY,
676
- weekDays: [
677
- 'MON',
678
- 'WED'
679
- ]
680
- })
681
- ]);
682
- const result = await service.getEventsForCalendarRange(queryDto({
683
- startDate: '2026-01-01',
684
- endDate: '2026-01-14'
685
- }), user);
686
- expect(result.map((e)=>e.eventDate)).toEqual([
687
- '2026-01-05',
688
- '2026-01-07',
689
- '2026-01-12',
690
- '2026-01-14'
691
- ]);
692
- expect(result[0].isRecurrenceInstance).toBe(false);
693
- expect(result.slice(1).every((e)=>e.isRecurrenceInstance === true)).toBe(true);
694
- });
695
- it('skips a weekday occurrence that falls before the series start date in its first week', async ()=>{
696
- // 2026-01-07 is a Wednesday; the same week's Monday (Jan 5) is before the series
697
- // start and must not produce an occurrence, but later weeks' Mondays should.
698
- mockEventQb.getMany.mockResolvedValue([
699
- buildEvent({
700
- eventDate: '2026-01-07',
701
- recurrenceType: _enums.RecurrenceType.WEEKLY,
702
- weekDays: [
703
- 'MON',
704
- 'WED'
705
- ]
706
- })
707
- ]);
708
- const result = await service.getEventsForCalendarRange(queryDto({
709
- startDate: '2026-01-01',
710
- endDate: '2026-01-21'
711
- }), user);
712
- expect(result.map((e)=>e.eventDate)).toEqual([
713
- '2026-01-07',
714
- '2026-01-12',
715
- '2026-01-14',
716
- '2026-01-19',
717
- '2026-01-21'
718
- ]);
719
- });
720
- it('expands a biweekly recurrence with two-week spacing', async ()=>{
721
- mockEventQb.getMany.mockResolvedValue([
722
- buildEvent({
723
- eventDate: '2026-01-05',
724
- recurrenceType: _enums.RecurrenceType.BIWEEKLY,
725
- weekDays: null
726
- })
727
- ]);
728
- const result = await service.getEventsForCalendarRange(queryDto({
729
- startDate: '2026-01-05',
730
- endDate: '2026-02-28'
731
- }), user);
732
- expect(result.map((e)=>e.eventDate)).toEqual([
733
- '2026-01-05',
734
- '2026-01-19',
735
- '2026-02-02',
736
- '2026-02-16'
737
- ]);
738
- });
739
- it('stops a weekly recurrence at the until bound mid-week', async ()=>{
740
- mockEventQb.getMany.mockResolvedValue([
741
- buildEvent({
742
- eventDate: '2026-01-05',
743
- recurrenceType: _enums.RecurrenceType.WEEKLY,
744
- weekDays: null,
745
- recurrenceEndDate: '2026-01-12'
746
- })
747
- ]);
748
- const result = await service.getEventsForCalendarRange(queryDto({
749
- startDate: '2026-01-01',
750
- endDate: '2026-01-31'
751
- }), user);
752
- expect(result.map((e)=>e.eventDate)).toEqual([
753
- '2026-01-05',
754
- '2026-01-12'
755
- ]);
756
- });
757
- // ---------------------------------------------------------------------
758
- // Recurrence expansion — monthly
759
- // ---------------------------------------------------------------------
760
- it('expands a monthly recurrence, clamping to the last day of shorter months', async ()=>{
761
- mockEventQb.getMany.mockResolvedValue([
762
- buildEvent({
763
- eventDate: '2026-01-31',
764
- recurrenceType: _enums.RecurrenceType.MONTHLY
765
- })
766
- ]);
767
- const result = await service.getEventsForCalendarRange(queryDto({
768
- startDate: '2026-01-01',
769
- endDate: '2026-04-30'
770
- }), user);
771
- // 2026 is not a leap year: Feb has 28 days; Mar has 31 (back to day 31); Apr has 30.
772
- expect(result.map((e)=>e.eventDate)).toEqual([
773
- '2026-01-31',
774
- '2026-02-28',
775
- '2026-03-31',
776
- '2026-04-30'
777
- ]);
778
- });
779
- it('restores the original day-of-month once a longer month is reached again', async ()=>{
780
- mockEventQb.getMany.mockResolvedValue([
781
- buildEvent({
782
- eventDate: '2026-01-31',
783
- recurrenceType: _enums.RecurrenceType.MONTHLY
784
- })
785
- ]);
786
- const result = await service.getEventsForCalendarRange(queryDto({
787
- startDate: '2026-02-01',
788
- endDate: '2026-03-31'
789
- }), user);
790
- expect(result.map((e)=>e.eventDate)).toEqual([
791
- '2026-02-28',
792
- '2026-03-31'
793
- ]);
794
- });
795
- it('clamps to Feb 29 on a leap year for a monthly recurrence starting Jan 31', async ()=>{
796
- mockEventQb.getMany.mockResolvedValue([
797
- buildEvent({
798
- eventDate: '2028-01-31',
799
- recurrenceType: _enums.RecurrenceType.MONTHLY
800
- })
801
- ]);
802
- const result = await service.getEventsForCalendarRange(queryDto({
803
- startDate: '2028-01-01',
804
- endDate: '2028-02-29'
805
- }), user);
806
- expect(result.map((e)=>e.eventDate)).toEqual([
807
- '2028-01-31',
808
- '2028-02-29'
809
- ]);
810
- });
811
- // ---------------------------------------------------------------------
812
- // All-day events
813
- // ---------------------------------------------------------------------
814
- it('preserves isAllDay through non-recurring expansion', async ()=>{
815
- mockEventQb.getMany.mockResolvedValue([
816
- buildEvent({
817
- eventDate: '2026-04-10',
818
- isAllDay: true,
819
- startTime: '00:00',
820
- endTime: '23:59'
821
- })
822
- ]);
823
- const result = await service.getEventsForCalendarRange(queryDto(), user);
824
- expect(result[0].isAllDay).toBe(true);
825
- expect(result[0].startTime).toBe('00:00');
826
- expect(result[0].endTime).toBe('23:59');
827
- });
828
- it('preserves isAllDay through recurring expansion', async ()=>{
829
- mockEventQb.getMany.mockResolvedValue([
830
- buildEvent({
831
- eventDate: '2026-01-01',
832
- recurrenceType: _enums.RecurrenceType.DAILY,
833
- isAllDay: true,
834
- startTime: '00:00',
835
- endTime: '23:59'
836
- })
837
- ]);
838
- const result = await service.getEventsForCalendarRange(queryDto({
839
- startDate: '2026-01-01',
840
- endDate: '2026-01-02'
841
- }), user);
842
- expect(result.every((e)=>e.isAllDay === true)).toBe(true);
843
- });
844
- // ---------------------------------------------------------------------
845
- // Sorting and multi-event ordering
846
- // ---------------------------------------------------------------------
847
- it('returns events sorted chronologically by eventDate, including expanded recurrence instances', async ()=>{
848
- mockEventQb.getMany.mockResolvedValue([
849
- buildEvent({
850
- id: 'event-b',
851
- eventDate: '2026-01-03',
852
- recurrenceType: _enums.RecurrenceType.NONE
853
- }),
854
- buildEvent({
855
- id: 'event-a',
856
- eventDate: '2026-01-01',
857
- recurrenceType: _enums.RecurrenceType.DAILY,
858
- recurrenceEndDate: '2026-01-02'
859
- })
860
- ]);
861
- const result = await service.getEventsForCalendarRange(queryDto({
862
- startDate: '2026-01-01',
863
- endDate: '2026-01-05'
864
- }), user);
865
- expect(result.map((e)=>e.eventDate)).toEqual([
866
- '2026-01-01',
867
- '2026-01-02',
868
- '2026-01-03'
869
- ]);
870
- });
871
- });
872
- describe('ensureParticipantRepositoryInitialized', ()=>{
873
- it('caches the participant repository after the first resolution', async ()=>{
874
- await service.ensureParticipantRepositoryInitialized();
875
- await service.ensureParticipantRepositoryInitialized();
876
- expect(mockDataSourceProvider.getRepository).toHaveBeenCalledTimes(1);
877
- expect(mockDataSourceProvider.getRepository).toHaveBeenCalledWith(_entities.EventParticipant);
878
- });
879
- });
880
- describe('domain events', ()=>{
881
- let publish;
882
- beforeEach(()=>{
883
- publish = jest.fn().mockResolvedValue(undefined);
884
- _classes.EventBusRegistry.reset();
885
- _classes.EventBusRegistry.setBus({
886
- publish,
887
- subscribe: jest.fn(),
888
- close: jest.fn()
889
- });
890
- _classes.EventBusRegistry.configureModule('event-manager', {
891
- enabled: true
892
- });
893
- });
894
- afterEach(()=>{
895
- _classes.EventBusRegistry.reset();
896
- });
897
- it('publishes event-manager.event.participant-status-changed with the new status', async ()=>{
898
- const participant = buildParticipant({
899
- status: _enums.ParticipantStatus.PENDING
900
- });
901
- mockParticipantRepo.findOne.mockResolvedValue(participant);
902
- mockParticipantRepo.save.mockImplementation(async (p)=>p);
903
- await service.updateParticipantStatus({
904
- id: 'participant-uuid-1',
905
- status: _enums.ParticipantStatus.ACCEPTED
906
- }, user);
907
- expect(publish).toHaveBeenCalledWith(expect.objectContaining({
908
- name: 'event-manager.event.participant-status-changed',
909
- payload: expect.objectContaining({
910
- metadata: expect.objectContaining({
911
- status: _enums.ParticipantStatus.ACCEPTED
912
- })
913
- })
914
- }));
915
- });
916
- it('does not publish when the event manager module has events disabled', async ()=>{
917
- _classes.EventBusRegistry.configureModule('event-manager', {
918
- enabled: false
919
- });
920
- mockParticipantRepo.findOne.mockResolvedValue(buildParticipant());
921
- mockParticipantRepo.save.mockImplementation(async (p)=>p);
922
- await service.updateParticipantStatus({
923
- id: 'participant-uuid-1',
924
- status: _enums.ParticipantStatus.ACCEPTED
925
- }, user);
926
- expect(publish).not.toHaveBeenCalled();
927
- });
928
- });
929
- });