@flusys/nestjs-task-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 (163) hide show
  1. package/config/index.d.ts +3 -3
  2. package/controllers/index.d.ts +5 -5
  3. package/controllers/task-board.controller.d.ts +3 -3
  4. package/controllers/task-comment.controller.d.ts +2 -2
  5. package/controllers/task-label.controller.d.ts +2 -2
  6. package/controllers/task-list.controller.d.ts +3 -3
  7. package/controllers/task.controller.d.ts +4 -4
  8. package/docs/index.d.ts +1 -1
  9. package/docs/task-manager-swagger.config.d.ts +1 -1
  10. package/dtos/index.d.ts +5 -5
  11. package/dtos/task-board.dto.d.ts +1 -1
  12. package/dtos/task.dto.d.ts +2 -2
  13. package/entities/index.d.ts +19 -19
  14. package/entities/task-board-with-company.entity.d.ts +1 -1
  15. package/fesm/234.js +1393 -0
  16. package/fesm/431.js +981 -0
  17. package/fesm/827.js +1054 -0
  18. package/fesm/988.js +881 -0
  19. package/fesm/config/index.js +156 -3
  20. package/fesm/controllers/index.js +21 -5
  21. package/fesm/docs/index.js +58 -1
  22. package/fesm/dtos/index.js +280 -5
  23. package/fesm/entities/index.js +26 -46
  24. package/fesm/gateways/index.js +288 -1
  25. package/fesm/index.js +440 -9
  26. package/fesm/interfaces/index.js +16 -2
  27. package/fesm/modules/index.js +210 -1
  28. package/fesm/runtime.js +85 -0
  29. package/fesm/services/index.js +53 -8
  30. package/gateways/index.d.ts +1 -1
  31. package/gateways/task-board.gateway.d.ts +3 -3
  32. package/index.d.ts +9 -9
  33. package/interfaces/index.d.ts +2 -2
  34. package/interfaces/task-manager-module.interface.d.ts +3 -3
  35. package/interfaces/task.interface.d.ts +2 -2
  36. package/modules/index.d.ts +1 -1
  37. package/modules/task-manager.module.d.ts +2 -2
  38. package/package.json +26 -36
  39. package/services/index.d.ts +8 -8
  40. package/services/task-activity.service.d.ts +1 -1
  41. package/services/task-board.service.d.ts +5 -5
  42. package/services/task-comment.service.d.ts +4 -4
  43. package/services/task-label.service.d.ts +4 -4
  44. package/services/task-list.service.d.ts +4 -4
  45. package/services/task-manager-config.service.d.ts +3 -3
  46. package/services/task-manager-datasource.provider.d.ts +3 -3
  47. package/services/task.service.d.ts +7 -7
  48. package/cjs/config/event-actions.js +0 -37
  49. package/cjs/config/index.js +0 -20
  50. package/cjs/config/message-keys.js +0 -92
  51. package/cjs/config/task-manager.constants.js +0 -11
  52. package/cjs/controllers/index.js +0 -22
  53. package/cjs/controllers/task-board.controller.js +0 -219
  54. package/cjs/controllers/task-board.controller.spec.js +0 -132
  55. package/cjs/controllers/task-comment.controller.js +0 -103
  56. package/cjs/controllers/task-comment.controller.spec.js +0 -86
  57. package/cjs/controllers/task-label.controller.js +0 -112
  58. package/cjs/controllers/task-label.controller.spec.js +0 -88
  59. package/cjs/controllers/task-list.controller.js +0 -158
  60. package/cjs/controllers/task-list.controller.spec.js +0 -112
  61. package/cjs/controllers/task.controller.js +0 -199
  62. package/cjs/controllers/task.controller.spec.js +0 -131
  63. package/cjs/docs/index.js +0 -18
  64. package/cjs/docs/task-manager-swagger.config.js +0 -43
  65. package/cjs/docs/task-manager-swagger.config.spec.js +0 -28
  66. package/cjs/dtos/index.js +0 -22
  67. package/cjs/dtos/task-board.dto.js +0 -310
  68. package/cjs/dtos/task-comment.dto.js +0 -131
  69. package/cjs/dtos/task-label.dto.js +0 -120
  70. package/cjs/dtos/task-list.dto.js +0 -198
  71. package/cjs/dtos/task.dto.js +0 -367
  72. package/cjs/entities/index.js +0 -72
  73. package/cjs/entities/index.spec.js +0 -30
  74. package/cjs/entities/task-activity.entity.js +0 -98
  75. package/cjs/entities/task-assignee.entity.js +0 -67
  76. package/cjs/entities/task-board-member.entity.js +0 -94
  77. package/cjs/entities/task-board-with-company.entity.js +0 -57
  78. package/cjs/entities/task-board.entity.js +0 -88
  79. package/cjs/entities/task-comment.entity.js +0 -80
  80. package/cjs/entities/task-label.entity.js +0 -72
  81. package/cjs/entities/task-list.entity.js +0 -104
  82. package/cjs/entities/task-task-label.entity.js +0 -67
  83. package/cjs/entities/task.entity.js +0 -151
  84. package/cjs/gateways/index.js +0 -18
  85. package/cjs/gateways/task-board.gateway.js +0 -167
  86. package/cjs/gateways/task-board.gateway.spec.js +0 -243
  87. package/cjs/index.js +0 -26
  88. package/cjs/interfaces/index.js +0 -19
  89. package/cjs/interfaces/task-manager-module.interface.js +0 -4
  90. package/cjs/interfaces/task.interface.js +0 -4
  91. package/cjs/modules/index.js +0 -18
  92. package/cjs/modules/task-manager.module.js +0 -135
  93. package/cjs/modules/task-manager.module.spec.js +0 -144
  94. package/cjs/services/index.js +0 -25
  95. package/cjs/services/task-activity.service.js +0 -76
  96. package/cjs/services/task-activity.service.spec.js +0 -125
  97. package/cjs/services/task-board.service.js +0 -247
  98. package/cjs/services/task-board.service.spec.js +0 -465
  99. package/cjs/services/task-comment.service.js +0 -103
  100. package/cjs/services/task-comment.service.spec.js +0 -160
  101. package/cjs/services/task-label.service.js +0 -97
  102. package/cjs/services/task-label.service.spec.js +0 -138
  103. package/cjs/services/task-list.service.js +0 -130
  104. package/cjs/services/task-list.service.spec.js +0 -284
  105. package/cjs/services/task-manager-config.service.js +0 -69
  106. package/cjs/services/task-manager-config.service.spec.js +0 -98
  107. package/cjs/services/task-manager-datasource.provider.js +0 -84
  108. package/cjs/services/task-manager-datasource.provider.spec.js +0 -131
  109. package/cjs/services/task.service.js +0 -398
  110. package/cjs/services/task.service.spec.js +0 -700
  111. package/fesm/config/event-actions.js +0 -16
  112. package/fesm/config/message-keys.js +0 -65
  113. package/fesm/config/task-manager.constants.js +0 -1
  114. package/fesm/controllers/task-board.controller.js +0 -209
  115. package/fesm/controllers/task-board.controller.spec.js +0 -128
  116. package/fesm/controllers/task-comment.controller.js +0 -93
  117. package/fesm/controllers/task-comment.controller.spec.js +0 -82
  118. package/fesm/controllers/task-label.controller.js +0 -102
  119. package/fesm/controllers/task-label.controller.spec.js +0 -84
  120. package/fesm/controllers/task-list.controller.js +0 -148
  121. package/fesm/controllers/task-list.controller.spec.js +0 -108
  122. package/fesm/controllers/task.controller.js +0 -189
  123. package/fesm/controllers/task.controller.spec.js +0 -127
  124. package/fesm/docs/task-manager-swagger.config.js +0 -33
  125. package/fesm/docs/task-manager-swagger.config.spec.js +0 -24
  126. package/fesm/dtos/task-board.dto.js +0 -277
  127. package/fesm/dtos/task-comment.dto.js +0 -110
  128. package/fesm/dtos/task-label.dto.js +0 -99
  129. package/fesm/dtos/task-list.dto.js +0 -174
  130. package/fesm/dtos/task.dto.js +0 -337
  131. package/fesm/entities/index.spec.js +0 -26
  132. package/fesm/entities/task-activity.entity.js +0 -88
  133. package/fesm/entities/task-assignee.entity.js +0 -57
  134. package/fesm/entities/task-board-member.entity.js +0 -76
  135. package/fesm/entities/task-board-with-company.entity.js +0 -47
  136. package/fesm/entities/task-board.entity.js +0 -78
  137. package/fesm/entities/task-comment.entity.js +0 -70
  138. package/fesm/entities/task-label.entity.js +0 -62
  139. package/fesm/entities/task-list.entity.js +0 -94
  140. package/fesm/entities/task-task-label.entity.js +0 -57
  141. package/fesm/entities/task.entity.js +0 -133
  142. package/fesm/gateways/task-board.gateway.js +0 -157
  143. package/fesm/gateways/task-board.gateway.spec.js +0 -239
  144. package/fesm/interfaces/task-manager-module.interface.js +0 -1
  145. package/fesm/interfaces/task.interface.js +0 -1
  146. package/fesm/modules/task-manager.module.js +0 -125
  147. package/fesm/modules/task-manager.module.spec.js +0 -140
  148. package/fesm/services/task-activity.service.js +0 -66
  149. package/fesm/services/task-activity.service.spec.js +0 -121
  150. package/fesm/services/task-board.service.js +0 -237
  151. package/fesm/services/task-board.service.spec.js +0 -461
  152. package/fesm/services/task-comment.service.js +0 -93
  153. package/fesm/services/task-comment.service.spec.js +0 -156
  154. package/fesm/services/task-label.service.js +0 -87
  155. package/fesm/services/task-label.service.spec.js +0 -134
  156. package/fesm/services/task-list.service.js +0 -120
  157. package/fesm/services/task-list.service.spec.js +0 -280
  158. package/fesm/services/task-manager-config.service.js +0 -59
  159. package/fesm/services/task-manager-config.service.spec.js +0 -94
  160. package/fesm/services/task-manager-datasource.provider.js +0 -74
  161. package/fesm/services/task-manager-datasource.provider.spec.js +0 -127
  162. package/fesm/services/task.service.js +0 -388
  163. package/fesm/services/task.service.spec.js +0 -696
@@ -1,700 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- const _testing = require("@nestjs/testing");
6
- const _classes = require("@flusys/nestjs-shared/classes");
7
- const _modules = require("@flusys/nestjs-shared/modules");
8
- const _repositorymock = require("@test-utils/mocks/repository.mock");
9
- const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
10
- const _entities = require("../entities");
11
- const _taskactivityservice = require("./task-activity.service");
12
- const _taskboardservice = require("./task-board.service");
13
- const _taskmanagerdatasourceprovider = require("./task-manager-datasource.provider");
14
- const _taskservice = require("./task.service");
15
- function buildTask(overrides = {}) {
16
- return {
17
- id: 'task-uuid-1',
18
- boardId: 'board-uuid-1',
19
- listId: 'list-uuid-1',
20
- title: 'Fix bug',
21
- description: null,
22
- priority: _entities.TaskPriority.MEDIUM,
23
- dueDate: null,
24
- position: 0,
25
- reporterId: null,
26
- parentTaskId: null,
27
- estimatedHours: null,
28
- assignees: [],
29
- createdAt: new Date(),
30
- updatedAt: new Date(),
31
- deletedAt: null,
32
- createdById: null,
33
- updatedById: null,
34
- deletedById: null,
35
- ...overrides
36
- };
37
- }
38
- function mockQueryRunner(repo) {
39
- const qr = {
40
- connect: jest.fn().mockResolvedValue(undefined),
41
- startTransaction: jest.fn().mockResolvedValue(undefined),
42
- commitTransaction: jest.fn().mockResolvedValue(undefined),
43
- rollbackTransaction: jest.fn().mockResolvedValue(undefined),
44
- release: jest.fn().mockResolvedValue(undefined),
45
- manager: {
46
- save: jest.fn((_target, entities)=>Promise.resolve(entities))
47
- }
48
- };
49
- repo.manager.connection.createQueryRunner.mockReturnValue(qr);
50
- return qr;
51
- }
52
- describe('TaskService', ()=>{
53
- const mockUtilsService = {
54
- clearCache: jest.fn(),
55
- getCacheKey: jest.fn().mockReturnValue('cache-key'),
56
- trackCacheKey: jest.fn()
57
- };
58
- // Call history is shared across tests in this file; reset it so each cache assertion is exact.
59
- beforeEach(()=>{
60
- mockUtilsService.clearCache.mockClear();
61
- });
62
- let service;
63
- let mockRepo;
64
- let mockLabelJunctionRepo;
65
- let mockFullLabelRepo;
66
- let mockAssigneeRepo;
67
- let mockBoardService;
68
- let mockActivityService;
69
- let mockDataSourceProvider;
70
- beforeEach(async ()=>{
71
- mockRepo = (0, _repositorymock.createMockRepository)();
72
- mockLabelJunctionRepo = (0, _repositorymock.createMockRepository)();
73
- mockFullLabelRepo = (0, _repositorymock.createMockRepository)();
74
- mockAssigneeRepo = (0, _repositorymock.createMockRepository)();
75
- mockLabelJunctionRepo.find.mockResolvedValue([]);
76
- mockFullLabelRepo.find.mockResolvedValue([]);
77
- mockAssigneeRepo.createQueryBuilder.mockReturnValue({
78
- ...(0, _repositorymock.createMockQueryBuilder)(),
79
- select: jest.fn().mockReturnThis(),
80
- addSelect: jest.fn().mockReturnThis(),
81
- leftJoin: jest.fn().mockReturnThis(),
82
- where: jest.fn().mockReturnThis(),
83
- getRawMany: jest.fn().mockResolvedValue([])
84
- });
85
- mockBoardService = {
86
- isBoardMember: jest.fn().mockResolvedValue(true)
87
- };
88
- mockActivityService = {
89
- log: jest.fn().mockResolvedValue(undefined)
90
- };
91
- mockDataSourceProvider = {
92
- getRepository: jest.fn((entity)=>{
93
- if (entity === _entities.TaskTaskLabel) return Promise.resolve(mockLabelJunctionRepo);
94
- if (entity === _entities.TaskLabel) return Promise.resolve(mockFullLabelRepo);
95
- if (entity === _entities.TaskAssignee) return Promise.resolve(mockAssigneeRepo);
96
- return Promise.resolve(mockRepo);
97
- }),
98
- getDataSource: jest.fn()
99
- };
100
- const module = await _testing.Test.createTestingModule({
101
- providers: [
102
- _taskservice.TaskService,
103
- {
104
- provide: 'CACHE_INSTANCE',
105
- useValue: {}
106
- },
107
- {
108
- provide: _modules.UtilsService,
109
- useValue: mockUtilsService
110
- },
111
- {
112
- provide: _taskmanagerdatasourceprovider.TaskManagerDataSourceProvider,
113
- useValue: mockDataSourceProvider
114
- },
115
- {
116
- provide: _taskboardservice.TaskBoardService,
117
- useValue: mockBoardService
118
- },
119
- {
120
- provide: _taskactivityservice.TaskActivityService,
121
- useValue: mockActivityService
122
- }
123
- ]
124
- }).compile();
125
- service = await module.resolve(_taskservice.TaskService);
126
- });
127
- describe('convertSingleDtoToEntity / insert (create path)', ()=>{
128
- it('creates a task with default priority, position 0 and reporterId from the current user', async ()=>{
129
- mockQueryRunner(mockRepo);
130
- const result = await service.insert({
131
- boardId: 'board-1',
132
- listId: 'list-1',
133
- title: 'New Task'
134
- }, (0, _loggedusermock.buildMockUser)({
135
- id: 'reporter-1'
136
- }));
137
- expect(result).toEqual(expect.objectContaining({
138
- boardId: 'board-1',
139
- listId: 'list-1',
140
- title: 'New Task',
141
- priority: 'MEDIUM',
142
- position: 0,
143
- reporterId: 'reporter-1'
144
- }));
145
- });
146
- it('respects an explicit priority and dueDate on create', async ()=>{
147
- mockQueryRunner(mockRepo);
148
- const result = await service.insert({
149
- boardId: 'board-1',
150
- listId: 'list-1',
151
- title: 'Urgent Task',
152
- priority: _entities.TaskPriority.URGENT,
153
- dueDate: '2026-12-31'
154
- }, (0, _loggedusermock.buildMockUser)());
155
- expect(result).toEqual(expect.objectContaining({
156
- priority: 'URGENT',
157
- dueDate: '2026-12-31'
158
- }));
159
- });
160
- it('validates every assignee is a board member and throws BadRequestException otherwise', async ()=>{
161
- mockQueryRunner(mockRepo);
162
- mockBoardService.isBoardMember.mockResolvedValueOnce(true).mockResolvedValueOnce(false);
163
- await expect(service.insert({
164
- boardId: 'board-1',
165
- listId: 'list-1',
166
- title: 'Task',
167
- assigneeIds: [
168
- 'member-1',
169
- 'stranger-1'
170
- ]
171
- }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
172
- });
173
- it('allows creation when all assignees are board members', async ()=>{
174
- mockQueryRunner(mockRepo);
175
- mockBoardService.isBoardMember.mockResolvedValue(true);
176
- await expect(service.insert({
177
- boardId: 'board-1',
178
- listId: 'list-1',
179
- title: 'Task',
180
- assigneeIds: [
181
- 'member-1'
182
- ]
183
- }, (0, _loggedusermock.buildMockUser)())).resolves.toBeDefined();
184
- expect(mockBoardService.isBoardMember).toHaveBeenCalledWith('board-1', 'member-1');
185
- });
186
- it('logs a "task.created" activity after a successful insert', async ()=>{
187
- mockQueryRunner(mockRepo);
188
- const result = await service.insert({
189
- boardId: 'board-1',
190
- listId: 'list-1',
191
- title: 'New Task'
192
- }, (0, _loggedusermock.buildMockUser)({
193
- id: 'reporter-1'
194
- }));
195
- expect(mockActivityService.log).toHaveBeenCalledWith(result.id, 'board-1', 'reporter-1', 'task.created', null, {
196
- title: 'New Task'
197
- });
198
- });
199
- it('syncs labels via the junction repository on create when labelIds are provided', async ()=>{
200
- mockQueryRunner(mockRepo);
201
- mockLabelJunctionRepo.create.mockImplementation((data)=>data);
202
- const result = await service.insert({
203
- boardId: 'board-1',
204
- listId: 'list-1',
205
- title: 'Labeled Task',
206
- labelIds: [
207
- 'label-1',
208
- 'label-2'
209
- ]
210
- }, (0, _loggedusermock.buildMockUser)());
211
- expect(mockLabelJunctionRepo.delete).toHaveBeenCalledWith({
212
- taskId: result.id
213
- });
214
- expect(mockLabelJunctionRepo.save).toHaveBeenCalledWith([
215
- expect.objectContaining({
216
- taskId: result.id,
217
- labelId: 'label-1'
218
- }),
219
- expect.objectContaining({
220
- taskId: result.id,
221
- labelId: 'label-2'
222
- })
223
- ]);
224
- });
225
- it('syncs assignees via the assignee repository on create when assigneeIds are provided', async ()=>{
226
- mockQueryRunner(mockRepo);
227
- mockAssigneeRepo.create.mockImplementation((data)=>data);
228
- const result = await service.insert({
229
- boardId: 'board-1',
230
- listId: 'list-1',
231
- title: 'Assigned Task',
232
- assigneeIds: [
233
- 'user-1'
234
- ]
235
- }, (0, _loggedusermock.buildMockUser)());
236
- expect(mockAssigneeRepo.save).toHaveBeenCalledWith([
237
- expect.objectContaining({
238
- taskId: result.id,
239
- userId: 'user-1'
240
- })
241
- ]);
242
- });
243
- });
244
- describe('convertSingleDtoToEntity / update (update path)', ()=>{
245
- it('throws NotFoundException when updating a task that does not exist', async ()=>{
246
- mockQueryRunner(mockRepo);
247
- mockRepo.findOne.mockResolvedValue(null);
248
- await expect(service.update({
249
- id: 'missing',
250
- title: 'Renamed'
251
- }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
252
- });
253
- it('merges update fields onto the existing entity, preserving position and reporterId', async ()=>{
254
- mockQueryRunner(mockRepo);
255
- const existing = buildTask({
256
- id: 'task-1',
257
- title: 'Old title',
258
- position: 7,
259
- reporterId: 'orig-reporter'
260
- });
261
- mockRepo.findOne.mockResolvedValue(existing);
262
- const result = await service.update({
263
- id: 'task-1',
264
- title: 'New title'
265
- }, (0, _loggedusermock.buildMockUser)());
266
- expect(result).toEqual(expect.objectContaining({
267
- id: 'task-1',
268
- title: 'New title',
269
- position: 7,
270
- reporterId: 'orig-reporter'
271
- }));
272
- });
273
- it('clears description when explicitly set to null on update', async ()=>{
274
- mockQueryRunner(mockRepo);
275
- const existing = buildTask({
276
- id: 'task-1',
277
- description: 'Old description'
278
- });
279
- mockRepo.findOne.mockResolvedValue(existing);
280
- const result = await service.update({
281
- id: 'task-1',
282
- description: null
283
- }, (0, _loggedusermock.buildMockUser)());
284
- expect(result.description).toBeNull();
285
- });
286
- it('re-validates assignee board membership when assigneeIds change on update', async ()=>{
287
- mockQueryRunner(mockRepo);
288
- const existing = buildTask({
289
- id: 'task-1'
290
- });
291
- mockRepo.findOne.mockResolvedValue(existing);
292
- mockBoardService.isBoardMember.mockResolvedValue(false);
293
- await expect(service.update({
294
- id: 'task-1',
295
- assigneeIds: [
296
- 'not-a-member'
297
- ]
298
- }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
299
- });
300
- it('only syncs labels/assignees on update when the respective dto fields are present', async ()=>{
301
- mockQueryRunner(mockRepo);
302
- const existing = buildTask({
303
- id: 'task-1'
304
- });
305
- mockRepo.findOne.mockResolvedValue(existing);
306
- await service.update({
307
- id: 'task-1',
308
- title: 'Just a rename'
309
- }, (0, _loggedusermock.buildMockUser)());
310
- expect(mockLabelJunctionRepo.delete).not.toHaveBeenCalled();
311
- expect(mockAssigneeRepo.delete).not.toHaveBeenCalled();
312
- });
313
- });
314
- describe('getFilterQuery', ()=>{
315
- it('filters by boardId, listId and priority using andWhere', async ()=>{
316
- const qb = (0, _repositorymock.createMockQueryBuilder)();
317
- await service.getFilterQuery(qb, {
318
- boardId: 'board-1',
319
- listId: 'list-1',
320
- priority: 'HIGH'
321
- }, (0, _loggedusermock.buildMockUser)());
322
- expect(qb.andWhere).toHaveBeenCalledWith('task.boardId = :boardId', {
323
- boardId: 'board-1'
324
- });
325
- expect(qb.andWhere).toHaveBeenCalledWith('task.listId = :listId', {
326
- listId: 'list-1'
327
- });
328
- expect(qb.andWhere).toHaveBeenCalledWith('task.priority = :priority', {
329
- priority: 'HIGH'
330
- });
331
- });
332
- it('joins the assignee table and filters by assigneeId', async ()=>{
333
- const qb = (0, _repositorymock.createMockQueryBuilder)();
334
- qb.innerJoin = jest.fn().mockReturnThis();
335
- await service.getFilterQuery(qb, {
336
- assigneeId: 'user-1'
337
- }, (0, _loggedusermock.buildMockUser)());
338
- expect(qb.innerJoin).toHaveBeenCalledWith('task_assignee', 'ta', 'ta.task_id = task.id');
339
- expect(qb.andWhere).toHaveBeenCalledWith('ta.user_id = :assigneeUserId', {
340
- assigneeUserId: 'user-1'
341
- });
342
- });
343
- it('ignores unknown filter keys', async ()=>{
344
- const qb = (0, _repositorymock.createMockQueryBuilder)();
345
- await service.getFilterQuery(qb, {
346
- unknownKey: 'value'
347
- }, (0, _loggedusermock.buildMockUser)());
348
- expect(qb.andWhere).not.toHaveBeenCalled();
349
- });
350
- });
351
- describe('moveTask', ()=>{
352
- it('moves a task to a target list and given position, logging "task.moved"', async ()=>{
353
- const task = buildTask({
354
- id: 'task-1',
355
- listId: 'list-old',
356
- boardId: 'board-1',
357
- position: 0
358
- });
359
- mockRepo.findOne.mockResolvedValue(task);
360
- mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
361
- const result = await service.moveTask({
362
- taskId: 'task-1',
363
- targetListId: 'list-new',
364
- position: 3
365
- }, (0, _loggedusermock.buildMockUser)({
366
- id: 'mover-1'
367
- }));
368
- expect(result.listId).toBe('list-new');
369
- expect(result.position).toBe(3);
370
- expect(result.updatedById).toBe('mover-1');
371
- expect(mockActivityService.log).toHaveBeenCalledWith('task-1', 'board-1', 'mover-1', 'task.moved', {
372
- listId: 'list-old'
373
- }, {
374
- listId: 'list-new'
375
- });
376
- });
377
- it('invalidates the task list cache and the moved task entry', async ()=>{
378
- const task = buildTask({
379
- id: 'task-1',
380
- listId: 'list-old',
381
- boardId: 'board-1'
382
- });
383
- mockRepo.findOne.mockResolvedValue(task);
384
- mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
385
- await service.moveTask({
386
- taskId: 'task-1',
387
- targetListId: 'list-new',
388
- position: 1
389
- }, (0, _loggedusermock.buildMockUser)({
390
- id: 'mover-1'
391
- }));
392
- expect(mockUtilsService.clearCache).toHaveBeenCalledWith('task', expect.anything());
393
- expect(mockUtilsService.clearCache).toHaveBeenCalledWith('task', expect.anything(), 'task-1');
394
- expect(mockUtilsService.clearCache).toHaveBeenCalledWith('task_board', expect.anything());
395
- });
396
- it('defaults position to 0 (first position) when no position is given', async ()=>{
397
- const task = buildTask({
398
- id: 'task-1',
399
- listId: 'list-old'
400
- });
401
- mockRepo.findOne.mockResolvedValue(task);
402
- mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
403
- const result = await service.moveTask({
404
- taskId: 'task-1',
405
- targetListId: 'list-new'
406
- }, (0, _loggedusermock.buildMockUser)());
407
- expect(result.position).toBe(0);
408
- });
409
- it('supports moving to the last position of a target list via an explicit high position value', async ()=>{
410
- const task = buildTask({
411
- id: 'task-1',
412
- listId: 'list-old'
413
- });
414
- mockRepo.findOne.mockResolvedValue(task);
415
- mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
416
- const result = await service.moveTask({
417
- taskId: 'task-1',
418
- targetListId: 'list-new',
419
- position: 99
420
- }, (0, _loggedusermock.buildMockUser)());
421
- expect(result.position).toBe(99);
422
- });
423
- it('supports moving within the same list (reordering, not a cross-list move)', async ()=>{
424
- const task = buildTask({
425
- id: 'task-1',
426
- listId: 'list-same',
427
- boardId: 'board-1',
428
- position: 0
429
- });
430
- mockRepo.findOne.mockResolvedValue(task);
431
- mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
432
- const result = await service.moveTask({
433
- taskId: 'task-1',
434
- targetListId: 'list-same',
435
- position: 2
436
- }, (0, _loggedusermock.buildMockUser)());
437
- expect(result.listId).toBe('list-same');
438
- expect(result.position).toBe(2);
439
- expect(mockActivityService.log).toHaveBeenCalledWith('task-1', 'board-1', expect.anything(), 'task.moved', {
440
- listId: 'list-same'
441
- }, {
442
- listId: 'list-same'
443
- });
444
- });
445
- it('throws NotFoundException when the task to move does not exist', async ()=>{
446
- mockRepo.findOne.mockResolvedValue(null);
447
- await expect(service.moveTask({
448
- taskId: 'missing',
449
- targetListId: 'list-new'
450
- }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
451
- });
452
- it('excludes soft-deleted tasks from being moved', async ()=>{
453
- mockRepo.findOne.mockResolvedValue(null);
454
- await expect(service.moveTask({
455
- taskId: 'deleted-task',
456
- targetListId: 'list-new'
457
- }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
458
- expect(mockRepo.findOne).toHaveBeenCalledWith({
459
- where: {
460
- id: 'deleted-task',
461
- deletedAt: expect.anything()
462
- }
463
- });
464
- });
465
- });
466
- describe('assignTask', ()=>{
467
- it('assigns board members to a task and logs "task.assigned" with old/new assignee ids', async ()=>{
468
- const task = buildTask({
469
- id: 'task-1',
470
- boardId: 'board-1'
471
- });
472
- mockRepo.findOne.mockResolvedValue(task);
473
- mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
474
- mockAssigneeRepo.find.mockResolvedValue([
475
- {
476
- userId: 'old-user'
477
- }
478
- ]);
479
- mockAssigneeRepo.create.mockImplementation((data)=>data);
480
- mockBoardService.isBoardMember.mockResolvedValue(true);
481
- const result = await service.assignTask({
482
- taskId: 'task-1',
483
- assigneeIds: [
484
- 'new-user-1',
485
- 'new-user-2'
486
- ]
487
- }, (0, _loggedusermock.buildMockUser)({
488
- id: 'assigner-1'
489
- }));
490
- expect(mockAssigneeRepo.delete).toHaveBeenCalledWith({
491
- taskId: 'task-1'
492
- });
493
- expect(mockAssigneeRepo.save).toHaveBeenCalledWith([
494
- expect.objectContaining({
495
- taskId: 'task-1',
496
- userId: 'new-user-1'
497
- }),
498
- expect.objectContaining({
499
- taskId: 'task-1',
500
- userId: 'new-user-2'
501
- })
502
- ]);
503
- expect(mockActivityService.log).toHaveBeenCalledWith('task-1', 'board-1', 'assigner-1', 'task.assigned', {
504
- assigneeIds: [
505
- 'old-user'
506
- ]
507
- }, {
508
- assigneeIds: [
509
- 'new-user-1',
510
- 'new-user-2'
511
- ]
512
- });
513
- expect(result.updatedById).toBe('assigner-1');
514
- });
515
- it('supports unassigning all users by passing an empty assigneeIds array', async ()=>{
516
- const task = buildTask({
517
- id: 'task-1',
518
- boardId: 'board-1'
519
- });
520
- mockRepo.findOne.mockResolvedValue(task);
521
- mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
522
- mockAssigneeRepo.find.mockResolvedValue([
523
- {
524
- userId: 'old-user'
525
- }
526
- ]);
527
- await service.assignTask({
528
- taskId: 'task-1',
529
- assigneeIds: []
530
- }, (0, _loggedusermock.buildMockUser)());
531
- expect(mockAssigneeRepo.delete).toHaveBeenCalledWith({
532
- taskId: 'task-1'
533
- });
534
- expect(mockAssigneeRepo.save).not.toHaveBeenCalled();
535
- });
536
- it('throws BadRequestException when any assignee is not a board member', async ()=>{
537
- const task = buildTask({
538
- id: 'task-1',
539
- boardId: 'board-1'
540
- });
541
- mockRepo.findOne.mockResolvedValue(task);
542
- mockBoardService.isBoardMember.mockResolvedValueOnce(true).mockResolvedValueOnce(false);
543
- await expect(service.assignTask({
544
- taskId: 'task-1',
545
- assigneeIds: [
546
- 'member-1',
547
- 'stranger-1'
548
- ]
549
- }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
550
- expect(mockAssigneeRepo.delete).not.toHaveBeenCalled();
551
- });
552
- it('throws NotFoundException when the task does not exist', async ()=>{
553
- mockRepo.findOne.mockResolvedValue(null);
554
- await expect(service.assignTask({
555
- taskId: 'missing',
556
- assigneeIds: [
557
- 'user-1'
558
- ]
559
- }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
560
- });
561
- it('returns the task with freshly loaded assignees populated', async ()=>{
562
- const task = buildTask({
563
- id: 'task-1',
564
- boardId: 'board-1'
565
- });
566
- mockRepo.findOne.mockResolvedValue(task);
567
- mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
568
- mockAssigneeRepo.find.mockResolvedValue([]);
569
- const rawRows = [
570
- {
571
- userId: 'new-user-1',
572
- userName: 'New User'
573
- }
574
- ];
575
- mockAssigneeRepo.createQueryBuilder.mockReturnValue({
576
- select: jest.fn().mockReturnThis(),
577
- addSelect: jest.fn().mockReturnThis(),
578
- leftJoin: jest.fn().mockReturnThis(),
579
- where: jest.fn().mockReturnThis(),
580
- getRawMany: jest.fn().mockResolvedValue(rawRows)
581
- });
582
- const result = await service.assignTask({
583
- taskId: 'task-1',
584
- assigneeIds: [
585
- 'new-user-1'
586
- ]
587
- }, (0, _loggedusermock.buildMockUser)());
588
- expect(result.assignees).toEqual([
589
- {
590
- userId: 'new-user-1',
591
- userName: 'New User'
592
- }
593
- ]);
594
- });
595
- });
596
- describe('getAll', ()=>{
597
- it('loads labels and assignees for every returned task', async ()=>{
598
- const tasks = [
599
- buildTask({
600
- id: 'task-1'
601
- }),
602
- buildTask({
603
- id: 'task-2'
604
- })
605
- ];
606
- const superGetAllSpy = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(service)), 'getAll').mockResolvedValue({
607
- data: tasks,
608
- total: 2
609
- });
610
- mockLabelJunctionRepo.find.mockResolvedValue([]);
611
- mockAssigneeRepo.createQueryBuilder.mockReturnValue({
612
- select: jest.fn().mockReturnThis(),
613
- addSelect: jest.fn().mockReturnThis(),
614
- leftJoin: jest.fn().mockReturnThis(),
615
- where: jest.fn().mockReturnThis(),
616
- getRawMany: jest.fn().mockResolvedValue([])
617
- });
618
- const result = await service.getAll('', {}, (0, _loggedusermock.buildMockUser)());
619
- expect(result.data).toHaveLength(2);
620
- expect(mockLabelJunctionRepo.find).toHaveBeenCalled();
621
- expect(mockAssigneeRepo.createQueryBuilder).toHaveBeenCalled();
622
- superGetAllSpy.mockRestore();
623
- });
624
- it('returns an empty result set without querying label/assignee tables when there are no tasks', async ()=>{
625
- const superGetAllSpy = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(service)), 'getAll').mockResolvedValue({
626
- data: [],
627
- total: 0
628
- });
629
- const result = await service.getAll('', {}, (0, _loggedusermock.buildMockUser)());
630
- expect(result.data).toEqual([]);
631
- expect(mockLabelJunctionRepo.find).not.toHaveBeenCalled();
632
- superGetAllSpy.mockRestore();
633
- });
634
- });
635
- describe('domain events', ()=>{
636
- let publish;
637
- beforeEach(()=>{
638
- publish = jest.fn().mockResolvedValue(undefined);
639
- _classes.EventBusRegistry.reset();
640
- _classes.EventBusRegistry.setBus({
641
- publish,
642
- subscribe: jest.fn(),
643
- close: jest.fn()
644
- });
645
- _classes.EventBusRegistry.configureModule('task-manager', {
646
- enabled: true
647
- });
648
- });
649
- afterEach(()=>{
650
- _classes.EventBusRegistry.reset();
651
- });
652
- it('publishes task-manager.task.moved with the source and target lists', async ()=>{
653
- const task = buildTask({
654
- id: 'task-1',
655
- listId: 'list-old',
656
- boardId: 'board-1',
657
- position: 0
658
- });
659
- mockRepo.findOne.mockResolvedValue(task);
660
- mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
661
- await service.moveTask({
662
- taskId: 'task-1',
663
- targetListId: 'list-new',
664
- position: 3
665
- }, (0, _loggedusermock.buildMockUser)({
666
- id: 'mover-1'
667
- }));
668
- expect(publish).toHaveBeenCalledWith(expect.objectContaining({
669
- name: 'task-manager.task.moved',
670
- payload: expect.objectContaining({
671
- ids: [
672
- 'task-1'
673
- ],
674
- metadata: expect.objectContaining({
675
- boardId: 'board-1',
676
- fromListId: 'list-old',
677
- toListId: 'list-new'
678
- })
679
- })
680
- }));
681
- });
682
- it('does not publish when the task manager module has events disabled', async ()=>{
683
- _classes.EventBusRegistry.configureModule('task-manager', {
684
- enabled: false
685
- });
686
- const task = buildTask({
687
- id: 'task-1',
688
- listId: 'list-old',
689
- boardId: 'board-1'
690
- });
691
- mockRepo.findOne.mockResolvedValue(task);
692
- mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
693
- await service.moveTask({
694
- taskId: 'task-1',
695
- targetListId: 'list-new'
696
- }, (0, _loggedusermock.buildMockUser)());
697
- expect(publish).not.toHaveBeenCalled();
698
- });
699
- });
700
- });