@flusys/nestjs-task-manager 7.1.0 → 8.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.
- package/config/index.d.ts +3 -3
- package/controllers/index.d.ts +5 -5
- package/controllers/task-board.controller.d.ts +3 -3
- package/controllers/task-comment.controller.d.ts +2 -2
- package/controllers/task-label.controller.d.ts +2 -2
- package/controllers/task-list.controller.d.ts +3 -3
- package/controllers/task.controller.d.ts +4 -4
- package/docs/index.d.ts +1 -1
- package/docs/task-manager-swagger.config.d.ts +1 -1
- package/dtos/index.d.ts +5 -5
- package/dtos/task-board.dto.d.ts +1 -1
- package/dtos/task.dto.d.ts +2 -2
- package/entities/index.d.ts +19 -19
- package/entities/task-board-with-company.entity.d.ts +1 -1
- package/fesm/234.js +1393 -0
- package/fesm/431.js +981 -0
- package/fesm/827.js +1054 -0
- package/fesm/988.js +881 -0
- package/fesm/config/index.js +156 -3
- package/fesm/controllers/index.js +21 -5
- package/fesm/docs/index.js +58 -1
- package/fesm/dtos/index.js +280 -5
- package/fesm/entities/index.js +26 -46
- package/fesm/gateways/index.js +288 -1
- package/fesm/index.js +440 -9
- package/fesm/interfaces/index.js +16 -2
- package/fesm/modules/index.js +210 -1
- package/fesm/runtime.js +85 -0
- package/fesm/services/index.js +53 -8
- package/gateways/index.d.ts +1 -1
- package/gateways/task-board.gateway.d.ts +3 -3
- package/index.d.ts +9 -9
- package/interfaces/index.d.ts +2 -2
- package/interfaces/task-manager-module.interface.d.ts +3 -3
- package/interfaces/task.interface.d.ts +2 -2
- package/modules/index.d.ts +1 -1
- package/modules/task-manager.module.d.ts +2 -2
- package/package.json +26 -36
- package/services/index.d.ts +8 -8
- package/services/task-activity.service.d.ts +1 -1
- package/services/task-board.service.d.ts +5 -5
- package/services/task-comment.service.d.ts +4 -4
- package/services/task-label.service.d.ts +4 -4
- package/services/task-list.service.d.ts +4 -4
- package/services/task-manager-config.service.d.ts +3 -3
- package/services/task-manager-datasource.provider.d.ts +3 -3
- package/services/task.service.d.ts +7 -7
- package/cjs/config/event-actions.js +0 -37
- package/cjs/config/index.js +0 -20
- package/cjs/config/message-keys.js +0 -92
- package/cjs/config/task-manager.constants.js +0 -11
- package/cjs/controllers/index.js +0 -22
- package/cjs/controllers/task-board.controller.js +0 -219
- package/cjs/controllers/task-board.controller.spec.js +0 -132
- package/cjs/controllers/task-comment.controller.js +0 -103
- package/cjs/controllers/task-comment.controller.spec.js +0 -86
- package/cjs/controllers/task-label.controller.js +0 -112
- package/cjs/controllers/task-label.controller.spec.js +0 -88
- package/cjs/controllers/task-list.controller.js +0 -158
- package/cjs/controllers/task-list.controller.spec.js +0 -112
- package/cjs/controllers/task.controller.js +0 -199
- package/cjs/controllers/task.controller.spec.js +0 -131
- package/cjs/docs/index.js +0 -18
- package/cjs/docs/task-manager-swagger.config.js +0 -43
- package/cjs/docs/task-manager-swagger.config.spec.js +0 -28
- package/cjs/dtos/index.js +0 -22
- package/cjs/dtos/task-board.dto.js +0 -310
- package/cjs/dtos/task-comment.dto.js +0 -131
- package/cjs/dtos/task-label.dto.js +0 -120
- package/cjs/dtos/task-list.dto.js +0 -198
- package/cjs/dtos/task.dto.js +0 -367
- package/cjs/entities/index.js +0 -72
- package/cjs/entities/index.spec.js +0 -30
- package/cjs/entities/task-activity.entity.js +0 -98
- package/cjs/entities/task-assignee.entity.js +0 -67
- package/cjs/entities/task-board-member.entity.js +0 -94
- package/cjs/entities/task-board-with-company.entity.js +0 -57
- package/cjs/entities/task-board.entity.js +0 -88
- package/cjs/entities/task-comment.entity.js +0 -80
- package/cjs/entities/task-label.entity.js +0 -72
- package/cjs/entities/task-list.entity.js +0 -104
- package/cjs/entities/task-task-label.entity.js +0 -67
- package/cjs/entities/task.entity.js +0 -151
- package/cjs/gateways/index.js +0 -18
- package/cjs/gateways/task-board.gateway.js +0 -167
- package/cjs/gateways/task-board.gateway.spec.js +0 -243
- package/cjs/index.js +0 -26
- package/cjs/interfaces/index.js +0 -19
- package/cjs/interfaces/task-manager-module.interface.js +0 -4
- package/cjs/interfaces/task.interface.js +0 -4
- package/cjs/modules/index.js +0 -18
- package/cjs/modules/task-manager.module.js +0 -135
- package/cjs/modules/task-manager.module.spec.js +0 -144
- package/cjs/services/index.js +0 -25
- package/cjs/services/task-activity.service.js +0 -76
- package/cjs/services/task-activity.service.spec.js +0 -125
- package/cjs/services/task-board.service.js +0 -247
- package/cjs/services/task-board.service.spec.js +0 -465
- package/cjs/services/task-comment.service.js +0 -103
- package/cjs/services/task-comment.service.spec.js +0 -160
- package/cjs/services/task-label.service.js +0 -97
- package/cjs/services/task-label.service.spec.js +0 -138
- package/cjs/services/task-list.service.js +0 -130
- package/cjs/services/task-list.service.spec.js +0 -284
- package/cjs/services/task-manager-config.service.js +0 -69
- package/cjs/services/task-manager-config.service.spec.js +0 -98
- package/cjs/services/task-manager-datasource.provider.js +0 -84
- package/cjs/services/task-manager-datasource.provider.spec.js +0 -131
- package/cjs/services/task.service.js +0 -398
- package/cjs/services/task.service.spec.js +0 -700
- package/fesm/config/event-actions.js +0 -16
- package/fesm/config/message-keys.js +0 -65
- package/fesm/config/task-manager.constants.js +0 -1
- package/fesm/controllers/task-board.controller.js +0 -209
- package/fesm/controllers/task-board.controller.spec.js +0 -128
- package/fesm/controllers/task-comment.controller.js +0 -93
- package/fesm/controllers/task-comment.controller.spec.js +0 -82
- package/fesm/controllers/task-label.controller.js +0 -102
- package/fesm/controllers/task-label.controller.spec.js +0 -84
- package/fesm/controllers/task-list.controller.js +0 -148
- package/fesm/controllers/task-list.controller.spec.js +0 -108
- package/fesm/controllers/task.controller.js +0 -189
- package/fesm/controllers/task.controller.spec.js +0 -127
- package/fesm/docs/task-manager-swagger.config.js +0 -33
- package/fesm/docs/task-manager-swagger.config.spec.js +0 -24
- package/fesm/dtos/task-board.dto.js +0 -277
- package/fesm/dtos/task-comment.dto.js +0 -110
- package/fesm/dtos/task-label.dto.js +0 -99
- package/fesm/dtos/task-list.dto.js +0 -174
- package/fesm/dtos/task.dto.js +0 -337
- package/fesm/entities/index.spec.js +0 -26
- package/fesm/entities/task-activity.entity.js +0 -88
- package/fesm/entities/task-assignee.entity.js +0 -57
- package/fesm/entities/task-board-member.entity.js +0 -76
- package/fesm/entities/task-board-with-company.entity.js +0 -47
- package/fesm/entities/task-board.entity.js +0 -78
- package/fesm/entities/task-comment.entity.js +0 -70
- package/fesm/entities/task-label.entity.js +0 -62
- package/fesm/entities/task-list.entity.js +0 -94
- package/fesm/entities/task-task-label.entity.js +0 -57
- package/fesm/entities/task.entity.js +0 -133
- package/fesm/gateways/task-board.gateway.js +0 -157
- package/fesm/gateways/task-board.gateway.spec.js +0 -239
- package/fesm/interfaces/task-manager-module.interface.js +0 -1
- package/fesm/interfaces/task.interface.js +0 -1
- package/fesm/modules/task-manager.module.js +0 -125
- package/fesm/modules/task-manager.module.spec.js +0 -140
- package/fesm/services/task-activity.service.js +0 -66
- package/fesm/services/task-activity.service.spec.js +0 -121
- package/fesm/services/task-board.service.js +0 -237
- package/fesm/services/task-board.service.spec.js +0 -461
- package/fesm/services/task-comment.service.js +0 -93
- package/fesm/services/task-comment.service.spec.js +0 -156
- package/fesm/services/task-label.service.js +0 -87
- package/fesm/services/task-label.service.spec.js +0 -134
- package/fesm/services/task-list.service.js +0 -120
- package/fesm/services/task-list.service.spec.js +0 -280
- package/fesm/services/task-manager-config.service.js +0 -59
- package/fesm/services/task-manager-config.service.spec.js +0 -94
- package/fesm/services/task-manager-datasource.provider.js +0 -74
- package/fesm/services/task-manager-datasource.provider.spec.js +0 -127
- package/fesm/services/task.service.js +0 -388
- package/fesm/services/task.service.spec.js +0 -696
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
import { Test } from '@nestjs/testing';
|
|
2
|
-
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
3
|
-
import { createMockQueryBuilder, createMockRepository } from '@test-utils/mocks/repository.mock';
|
|
4
|
-
import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
5
|
-
import { BoardMemberRole, TaskBoard, TaskBoardMember, TaskBoardWithCompany } from '../entities';
|
|
6
|
-
import { TaskBoardService } from './task-board.service';
|
|
7
|
-
import { TaskManagerConfigService } from './task-manager-config.service';
|
|
8
|
-
import { TaskManagerDataSourceProvider } from './task-manager-datasource.provider';
|
|
9
|
-
function buildBoard(overrides = {}) {
|
|
10
|
-
return {
|
|
11
|
-
id: 'board-uuid-1',
|
|
12
|
-
name: 'Sprint Board',
|
|
13
|
-
description: null,
|
|
14
|
-
color: '#3B82F6',
|
|
15
|
-
isActive: true,
|
|
16
|
-
settings: null,
|
|
17
|
-
createdAt: new Date(),
|
|
18
|
-
updatedAt: new Date(),
|
|
19
|
-
deletedAt: null,
|
|
20
|
-
createdById: null,
|
|
21
|
-
updatedById: null,
|
|
22
|
-
deletedById: null,
|
|
23
|
-
...overrides
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
function buildMember(overrides = {}) {
|
|
27
|
-
return {
|
|
28
|
-
id: 'member-uuid-1',
|
|
29
|
-
boardId: 'board-uuid-1',
|
|
30
|
-
userId: 'user-uuid-1',
|
|
31
|
-
role: BoardMemberRole.MEMBER,
|
|
32
|
-
createdAt: new Date(),
|
|
33
|
-
updatedAt: new Date(),
|
|
34
|
-
deletedAt: null,
|
|
35
|
-
createdById: null,
|
|
36
|
-
updatedById: null,
|
|
37
|
-
deletedById: null,
|
|
38
|
-
...overrides
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function mockQueryRunner(repo) {
|
|
42
|
-
const qr = {
|
|
43
|
-
connect: jest.fn().mockResolvedValue(undefined),
|
|
44
|
-
startTransaction: jest.fn().mockResolvedValue(undefined),
|
|
45
|
-
commitTransaction: jest.fn().mockResolvedValue(undefined),
|
|
46
|
-
rollbackTransaction: jest.fn().mockResolvedValue(undefined),
|
|
47
|
-
release: jest.fn().mockResolvedValue(undefined),
|
|
48
|
-
manager: {
|
|
49
|
-
save: jest.fn((_target, entities)=>Promise.resolve(entities))
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
repo.manager.connection.createQueryRunner.mockReturnValue(qr);
|
|
53
|
-
return qr;
|
|
54
|
-
}
|
|
55
|
-
describe('TaskBoardService', ()=>{
|
|
56
|
-
const mockUtilsService = {
|
|
57
|
-
clearCache: jest.fn(),
|
|
58
|
-
getCacheKey: jest.fn().mockReturnValue('cache-key'),
|
|
59
|
-
trackCacheKey: jest.fn()
|
|
60
|
-
};
|
|
61
|
-
// Call history is shared across tests in this file; reset it so each cache assertion is exact.
|
|
62
|
-
beforeEach(()=>{
|
|
63
|
-
mockUtilsService.clearCache.mockClear();
|
|
64
|
-
});
|
|
65
|
-
let service;
|
|
66
|
-
let module;
|
|
67
|
-
let mockRepo;
|
|
68
|
-
let mockMemberRepo;
|
|
69
|
-
let mockConfig;
|
|
70
|
-
let mockDataSourceProvider;
|
|
71
|
-
beforeEach(async ()=>{
|
|
72
|
-
mockRepo = createMockRepository();
|
|
73
|
-
mockMemberRepo = createMockRepository();
|
|
74
|
-
mockConfig = {
|
|
75
|
-
isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
|
|
76
|
-
};
|
|
77
|
-
mockDataSourceProvider = {
|
|
78
|
-
getRepository: jest.fn((entity)=>{
|
|
79
|
-
if (entity === TaskBoardMember) return Promise.resolve(mockMemberRepo);
|
|
80
|
-
return Promise.resolve(mockRepo);
|
|
81
|
-
}),
|
|
82
|
-
getDataSource: jest.fn()
|
|
83
|
-
};
|
|
84
|
-
module = await Test.createTestingModule({
|
|
85
|
-
providers: [
|
|
86
|
-
TaskBoardService,
|
|
87
|
-
{
|
|
88
|
-
provide: 'CACHE_INSTANCE',
|
|
89
|
-
useValue: {}
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
provide: UtilsService,
|
|
93
|
-
useValue: mockUtilsService
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
provide: TaskManagerConfigService,
|
|
97
|
-
useValue: mockConfig
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
provide: TaskManagerDataSourceProvider,
|
|
101
|
-
useValue: mockDataSourceProvider
|
|
102
|
-
}
|
|
103
|
-
]
|
|
104
|
-
}).compile();
|
|
105
|
-
service = await module.resolve(TaskBoardService);
|
|
106
|
-
});
|
|
107
|
-
describe('entity selection', ()=>{
|
|
108
|
-
it('uses TaskBoard when the company feature is disabled', async ()=>{
|
|
109
|
-
mockConfig.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
110
|
-
const built = await module.resolve(TaskBoardService);
|
|
111
|
-
expect(built['_entity']).toBe(TaskBoard);
|
|
112
|
-
});
|
|
113
|
-
it('uses TaskBoardWithCompany when the company feature is enabled', async ()=>{
|
|
114
|
-
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
115
|
-
const built = await module.resolve(TaskBoardService);
|
|
116
|
-
expect(built['_entity']).toBe(TaskBoardWithCompany);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
describe('convertSingleDtoToEntity (via insert/update)', ()=>{
|
|
120
|
-
it('creates a board applying default color and active flag', async ()=>{
|
|
121
|
-
mockQueryRunner(mockRepo);
|
|
122
|
-
const result = await service.insert({
|
|
123
|
-
name: 'New Board'
|
|
124
|
-
}, buildMockUser());
|
|
125
|
-
expect(result).toEqual(expect.objectContaining({
|
|
126
|
-
name: 'New Board',
|
|
127
|
-
color: '#3B82F6',
|
|
128
|
-
isActive: true
|
|
129
|
-
}));
|
|
130
|
-
expect(result.companyId).toBeUndefined();
|
|
131
|
-
});
|
|
132
|
-
it('stamps companyId from the user when the company feature is enabled', async ()=>{
|
|
133
|
-
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
134
|
-
mockQueryRunner(mockRepo);
|
|
135
|
-
const result = await service.insert({
|
|
136
|
-
name: 'Company Board'
|
|
137
|
-
}, buildMockUser({
|
|
138
|
-
companyId: 'company-9'
|
|
139
|
-
}));
|
|
140
|
-
expect(result.companyId).toBe('company-9');
|
|
141
|
-
});
|
|
142
|
-
it('throws NotFoundException when updating a non-existent board', async ()=>{
|
|
143
|
-
mockQueryRunner(mockRepo);
|
|
144
|
-
mockRepo.findOne.mockResolvedValue(null);
|
|
145
|
-
await expect(service.update({
|
|
146
|
-
id: 'missing',
|
|
147
|
-
name: 'Renamed'
|
|
148
|
-
}, buildMockUser())).rejects.toThrow();
|
|
149
|
-
});
|
|
150
|
-
it('merges update fields, preserving description as null when explicitly cleared', async ()=>{
|
|
151
|
-
mockQueryRunner(mockRepo);
|
|
152
|
-
const existing = buildBoard({
|
|
153
|
-
id: 'board-1',
|
|
154
|
-
description: 'Old description'
|
|
155
|
-
});
|
|
156
|
-
mockRepo.findOne.mockResolvedValue(existing);
|
|
157
|
-
const result = await service.update({
|
|
158
|
-
id: 'board-1',
|
|
159
|
-
description: null
|
|
160
|
-
}, buildMockUser());
|
|
161
|
-
expect(result.description).toBeNull();
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
describe('afterInsertOperation', ()=>{
|
|
165
|
-
it('adds the creating user as an OWNER member of every newly created board', async ()=>{
|
|
166
|
-
mockQueryRunner(mockRepo);
|
|
167
|
-
mockMemberRepo.create.mockImplementation((data)=>data);
|
|
168
|
-
mockMemberRepo.save.mockResolvedValue([]);
|
|
169
|
-
await service.insert({
|
|
170
|
-
name: 'New Board'
|
|
171
|
-
}, buildMockUser({
|
|
172
|
-
id: 'creator-1'
|
|
173
|
-
}));
|
|
174
|
-
expect(mockMemberRepo.save).toHaveBeenCalledWith([
|
|
175
|
-
expect.objectContaining({
|
|
176
|
-
userId: 'creator-1',
|
|
177
|
-
role: BoardMemberRole.OWNER,
|
|
178
|
-
createdById: 'creator-1'
|
|
179
|
-
})
|
|
180
|
-
]);
|
|
181
|
-
});
|
|
182
|
-
it('does nothing when no user is present (system-created board)', async ()=>{
|
|
183
|
-
mockQueryRunner(mockRepo);
|
|
184
|
-
await service.insert({
|
|
185
|
-
name: 'System Board'
|
|
186
|
-
}, null);
|
|
187
|
-
expect(mockMemberRepo.save).not.toHaveBeenCalled();
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
describe('getExtraManipulateQuery', ()=>{
|
|
191
|
-
it('applies the company filter, current-user membership join, and aggregate subselects', async ()=>{
|
|
192
|
-
const qb = createMockQueryBuilder();
|
|
193
|
-
qb.innerJoin = jest.fn().mockReturnThis();
|
|
194
|
-
qb.addSelect = jest.fn().mockReturnThis();
|
|
195
|
-
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
196
|
-
const user = buildMockUser({
|
|
197
|
-
id: 'user-1',
|
|
198
|
-
companyId: 'company-1'
|
|
199
|
-
});
|
|
200
|
-
const { isRaw } = await service.getExtraManipulateQuery(qb, {}, user);
|
|
201
|
-
expect(qb.andWhere).toHaveBeenCalledWith('task_board.companyId = :companyId', {
|
|
202
|
-
companyId: 'company-1'
|
|
203
|
-
});
|
|
204
|
-
expect(qb.innerJoin).toHaveBeenCalledWith('task_board_member', 'tbm_filter', expect.stringContaining('tbm_filter.user_id = :currentUserId'), {
|
|
205
|
-
currentUserId: 'user-1'
|
|
206
|
-
});
|
|
207
|
-
expect(qb.addSelect).toHaveBeenCalledWith(expect.stringContaining('task_board_member'), 'total_members');
|
|
208
|
-
expect(qb.addSelect).toHaveBeenCalledWith(expect.stringContaining('task_list'), 'total_lists');
|
|
209
|
-
expect(qb.addSelect).toHaveBeenCalledWith(expect.stringContaining('FROM task t'), 'total_tasks');
|
|
210
|
-
expect(isRaw).toBe(true);
|
|
211
|
-
});
|
|
212
|
-
it('skips the membership join when no user is provided', async ()=>{
|
|
213
|
-
const qb = createMockQueryBuilder();
|
|
214
|
-
qb.innerJoin = jest.fn().mockReturnThis();
|
|
215
|
-
qb.addSelect = jest.fn().mockReturnThis();
|
|
216
|
-
await service.getExtraManipulateQuery(qb, {}, null);
|
|
217
|
-
expect(qb.innerJoin).not.toHaveBeenCalled();
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
describe('convertEntityToResponseDto', ()=>{
|
|
221
|
-
it('passes through non-raw entities unchanged (default behavior)', ()=>{
|
|
222
|
-
const board = buildBoard();
|
|
223
|
-
const result = service.convertEntityToResponseDto(board, false);
|
|
224
|
-
expect(result).toEqual(board);
|
|
225
|
-
});
|
|
226
|
-
it('strips the entity-name prefix and camelCases raw aggregate columns, converting numeric strings', ()=>{
|
|
227
|
-
const raw = {
|
|
228
|
-
task_board_id: 'board-1',
|
|
229
|
-
task_board_name: 'Sprint Board',
|
|
230
|
-
total_members: '3',
|
|
231
|
-
total_lists: '2'
|
|
232
|
-
};
|
|
233
|
-
const result = service.convertEntityToResponseDto(raw, true);
|
|
234
|
-
expect(result).toEqual({
|
|
235
|
-
id: 'board-1',
|
|
236
|
-
name: 'Sprint Board',
|
|
237
|
-
totalMembers: 3,
|
|
238
|
-
totalLists: 2
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
describe('addMember', ()=>{
|
|
243
|
-
it('adds a new member with the requested role', async ()=>{
|
|
244
|
-
mockRepo.findOne.mockResolvedValue(buildBoard({
|
|
245
|
-
id: 'board-1'
|
|
246
|
-
}));
|
|
247
|
-
mockMemberRepo.findOne.mockResolvedValueOnce(null); // no active existing member
|
|
248
|
-
mockMemberRepo.findOne.mockResolvedValueOnce(null); // no soft-deleted member either
|
|
249
|
-
mockMemberRepo.create.mockImplementation((data)=>data);
|
|
250
|
-
const created = buildMember({
|
|
251
|
-
boardId: 'board-1',
|
|
252
|
-
userId: 'user-2',
|
|
253
|
-
role: BoardMemberRole.ADMIN
|
|
254
|
-
});
|
|
255
|
-
mockMemberRepo.save.mockResolvedValue(created);
|
|
256
|
-
const result = await service.addMember({
|
|
257
|
-
boardId: 'board-1',
|
|
258
|
-
userId: 'user-2',
|
|
259
|
-
role: BoardMemberRole.ADMIN
|
|
260
|
-
}, buildMockUser({
|
|
261
|
-
id: 'owner-1'
|
|
262
|
-
}));
|
|
263
|
-
expect(mockMemberRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
264
|
-
boardId: 'board-1',
|
|
265
|
-
userId: 'user-2',
|
|
266
|
-
role: BoardMemberRole.ADMIN,
|
|
267
|
-
createdById: 'owner-1'
|
|
268
|
-
}));
|
|
269
|
-
expect(result).toEqual(created);
|
|
270
|
-
});
|
|
271
|
-
it('invalidates the board list cache and the board entry (membership drives visibility and total_members)', async ()=>{
|
|
272
|
-
mockRepo.findOne.mockResolvedValue(buildBoard({
|
|
273
|
-
id: 'board-1'
|
|
274
|
-
}));
|
|
275
|
-
mockMemberRepo.findOne.mockResolvedValueOnce(null);
|
|
276
|
-
mockMemberRepo.findOne.mockResolvedValueOnce(null);
|
|
277
|
-
mockMemberRepo.create.mockImplementation((data)=>data);
|
|
278
|
-
mockMemberRepo.save.mockResolvedValue(buildMember({
|
|
279
|
-
boardId: 'board-1'
|
|
280
|
-
}));
|
|
281
|
-
await service.addMember({
|
|
282
|
-
boardId: 'board-1',
|
|
283
|
-
userId: 'user-2'
|
|
284
|
-
}, buildMockUser());
|
|
285
|
-
expect(mockUtilsService.clearCache).toHaveBeenCalledWith('task_board', expect.anything());
|
|
286
|
-
expect(mockUtilsService.clearCache).toHaveBeenCalledWith('task_board', expect.anything(), 'board-1');
|
|
287
|
-
});
|
|
288
|
-
it('defaults the role to MEMBER when none is given', async ()=>{
|
|
289
|
-
mockRepo.findOne.mockResolvedValue(buildBoard({
|
|
290
|
-
id: 'board-1'
|
|
291
|
-
}));
|
|
292
|
-
mockMemberRepo.findOne.mockResolvedValueOnce(null);
|
|
293
|
-
mockMemberRepo.findOne.mockResolvedValueOnce(null);
|
|
294
|
-
mockMemberRepo.create.mockImplementation((data)=>data);
|
|
295
|
-
mockMemberRepo.save.mockResolvedValue(buildMember());
|
|
296
|
-
await service.addMember({
|
|
297
|
-
boardId: 'board-1',
|
|
298
|
-
userId: 'user-2'
|
|
299
|
-
}, buildMockUser());
|
|
300
|
-
expect(mockMemberRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
301
|
-
role: BoardMemberRole.MEMBER
|
|
302
|
-
}));
|
|
303
|
-
});
|
|
304
|
-
it('throws NotFoundException when the board does not exist', async ()=>{
|
|
305
|
-
mockRepo.findOne.mockResolvedValue(null);
|
|
306
|
-
await expect(service.addMember({
|
|
307
|
-
boardId: 'missing-board',
|
|
308
|
-
userId: 'user-2'
|
|
309
|
-
}, buildMockUser())).rejects.toThrow();
|
|
310
|
-
});
|
|
311
|
-
it('throws BadRequestException when the user is already an active member', async ()=>{
|
|
312
|
-
mockRepo.findOne.mockResolvedValue(buildBoard({
|
|
313
|
-
id: 'board-1'
|
|
314
|
-
}));
|
|
315
|
-
mockMemberRepo.findOne.mockResolvedValueOnce(buildMember());
|
|
316
|
-
await expect(service.addMember({
|
|
317
|
-
boardId: 'board-1',
|
|
318
|
-
userId: 'user-uuid-1'
|
|
319
|
-
}, buildMockUser())).rejects.toThrow();
|
|
320
|
-
});
|
|
321
|
-
it('restores a soft-deleted membership instead of creating a duplicate row', async ()=>{
|
|
322
|
-
mockRepo.findOne.mockResolvedValue(buildBoard({
|
|
323
|
-
id: 'board-1'
|
|
324
|
-
}));
|
|
325
|
-
mockMemberRepo.findOne.mockResolvedValueOnce(null); // no active member
|
|
326
|
-
const softDeleted = buildMember({
|
|
327
|
-
id: 'member-old',
|
|
328
|
-
deletedAt: new Date()
|
|
329
|
-
});
|
|
330
|
-
mockMemberRepo.findOne.mockResolvedValueOnce(softDeleted); // soft-deleted row found
|
|
331
|
-
mockMemberRepo.restore.mockResolvedValue(undefined);
|
|
332
|
-
mockMemberRepo.update.mockResolvedValue(undefined);
|
|
333
|
-
const restored = buildMember({
|
|
334
|
-
id: 'member-old',
|
|
335
|
-
role: BoardMemberRole.ADMIN
|
|
336
|
-
});
|
|
337
|
-
mockMemberRepo.findOne.mockResolvedValueOnce(restored); // re-fetch after restore
|
|
338
|
-
const result = await service.addMember({
|
|
339
|
-
boardId: 'board-1',
|
|
340
|
-
userId: 'user-uuid-1',
|
|
341
|
-
role: BoardMemberRole.ADMIN
|
|
342
|
-
}, buildMockUser());
|
|
343
|
-
expect(mockMemberRepo.restore).toHaveBeenCalledWith({
|
|
344
|
-
id: 'member-old'
|
|
345
|
-
});
|
|
346
|
-
expect(mockMemberRepo.update).toHaveBeenCalledWith({
|
|
347
|
-
id: 'member-old'
|
|
348
|
-
}, {
|
|
349
|
-
role: BoardMemberRole.ADMIN
|
|
350
|
-
});
|
|
351
|
-
expect(mockMemberRepo.create).not.toHaveBeenCalled();
|
|
352
|
-
expect(result).toEqual(restored);
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
describe('removeMember', ()=>{
|
|
356
|
-
it('soft-deletes an existing member', async ()=>{
|
|
357
|
-
const member = buildMember({
|
|
358
|
-
id: 'member-1'
|
|
359
|
-
});
|
|
360
|
-
mockMemberRepo.findOne.mockResolvedValue(member);
|
|
361
|
-
await service.removeMember({
|
|
362
|
-
boardId: 'board-1',
|
|
363
|
-
userId: 'user-uuid-1'
|
|
364
|
-
});
|
|
365
|
-
expect(mockMemberRepo.update).toHaveBeenCalledWith({
|
|
366
|
-
id: 'member-1'
|
|
367
|
-
}, {
|
|
368
|
-
deletedAt: expect.any(Date)
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
it('invalidates the board list cache and the board entry', async ()=>{
|
|
372
|
-
mockMemberRepo.findOne.mockResolvedValue(buildMember({
|
|
373
|
-
id: 'member-1'
|
|
374
|
-
}));
|
|
375
|
-
await service.removeMember({
|
|
376
|
-
boardId: 'board-1',
|
|
377
|
-
userId: 'user-uuid-1'
|
|
378
|
-
});
|
|
379
|
-
expect(mockUtilsService.clearCache).toHaveBeenCalledWith('task_board', expect.anything());
|
|
380
|
-
expect(mockUtilsService.clearCache).toHaveBeenCalledWith('task_board', expect.anything(), 'board-1');
|
|
381
|
-
});
|
|
382
|
-
it('throws NotFoundException when the member is not found', async ()=>{
|
|
383
|
-
mockMemberRepo.findOne.mockResolvedValue(null);
|
|
384
|
-
await expect(service.removeMember({
|
|
385
|
-
boardId: 'board-1',
|
|
386
|
-
userId: 'unknown-user'
|
|
387
|
-
})).rejects.toThrow();
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
function buildMembersQueryBuilder() {
|
|
391
|
-
const qb = createMockQueryBuilder();
|
|
392
|
-
qb.leftJoin = jest.fn().mockReturnThis();
|
|
393
|
-
qb.addSelect = jest.fn().mockReturnThis();
|
|
394
|
-
qb.getRawAndEntities = jest.fn();
|
|
395
|
-
return qb;
|
|
396
|
-
}
|
|
397
|
-
describe('getMembers', ()=>{
|
|
398
|
-
it('returns members enriched with joined user name/email', async ()=>{
|
|
399
|
-
const qb = buildMembersQueryBuilder();
|
|
400
|
-
qb.getRawAndEntities.mockResolvedValue({
|
|
401
|
-
entities: [
|
|
402
|
-
buildMember({
|
|
403
|
-
id: 'member-1',
|
|
404
|
-
userId: 'user-1'
|
|
405
|
-
})
|
|
406
|
-
],
|
|
407
|
-
raw: [
|
|
408
|
-
{
|
|
409
|
-
userName: 'Alice',
|
|
410
|
-
userEmail: 'alice@example.com'
|
|
411
|
-
}
|
|
412
|
-
]
|
|
413
|
-
});
|
|
414
|
-
mockMemberRepo.createQueryBuilder.mockReturnValue(qb);
|
|
415
|
-
const result = await service.getMembers('board-1');
|
|
416
|
-
expect(result).toEqual([
|
|
417
|
-
expect.objectContaining({
|
|
418
|
-
id: 'member-1',
|
|
419
|
-
userName: 'Alice',
|
|
420
|
-
userEmail: 'alice@example.com'
|
|
421
|
-
})
|
|
422
|
-
]);
|
|
423
|
-
});
|
|
424
|
-
it('defaults userName/userEmail to null when the joined user row is missing', async ()=>{
|
|
425
|
-
const qb = buildMembersQueryBuilder();
|
|
426
|
-
qb.getRawAndEntities.mockResolvedValue({
|
|
427
|
-
entities: [
|
|
428
|
-
buildMember({
|
|
429
|
-
id: 'member-1'
|
|
430
|
-
})
|
|
431
|
-
],
|
|
432
|
-
raw: [
|
|
433
|
-
{}
|
|
434
|
-
]
|
|
435
|
-
});
|
|
436
|
-
mockMemberRepo.createQueryBuilder.mockReturnValue(qb);
|
|
437
|
-
const result = await service.getMembers('board-1');
|
|
438
|
-
expect(result[0].userName).toBeNull();
|
|
439
|
-
expect(result[0].userEmail).toBeNull();
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
describe('isBoardMember', ()=>{
|
|
443
|
-
it('returns true when an active membership row exists', async ()=>{
|
|
444
|
-
mockMemberRepo.count.mockResolvedValue(1);
|
|
445
|
-
const result = await service.isBoardMember('board-1', 'user-1');
|
|
446
|
-
expect(result).toBe(true);
|
|
447
|
-
expect(mockMemberRepo.count).toHaveBeenCalledWith({
|
|
448
|
-
where: {
|
|
449
|
-
boardId: 'board-1',
|
|
450
|
-
userId: 'user-1',
|
|
451
|
-
deletedAt: expect.anything()
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
});
|
|
455
|
-
it('returns false when no membership row exists', async ()=>{
|
|
456
|
-
mockMemberRepo.count.mockResolvedValue(0);
|
|
457
|
-
const result = await service.isBoardMember('board-1', 'stranger');
|
|
458
|
-
expect(result).toBe(false);
|
|
459
|
-
});
|
|
460
|
-
});
|
|
461
|
-
});
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
function _define_property(obj, key, value) {
|
|
2
|
-
if (key in obj) {
|
|
3
|
-
Object.defineProperty(obj, key, {
|
|
4
|
-
value: value,
|
|
5
|
-
enumerable: true,
|
|
6
|
-
configurable: true,
|
|
7
|
-
writable: true
|
|
8
|
-
});
|
|
9
|
-
} else {
|
|
10
|
-
obj[key] = value;
|
|
11
|
-
}
|
|
12
|
-
return obj;
|
|
13
|
-
}
|
|
14
|
-
function _ts_decorate(decorators, target, key, desc) {
|
|
15
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
17
|
-
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
18
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
|
-
}
|
|
20
|
-
function _ts_metadata(k, v) {
|
|
21
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
-
}
|
|
23
|
-
function _ts_param(paramIndex, decorator) {
|
|
24
|
-
return function(target, key) {
|
|
25
|
-
decorator(target, key, paramIndex);
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
|
|
29
|
-
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
-
import { ForbiddenException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
|
|
31
|
-
import { TASK_COMMENT_MESSAGES, TASK_MANAGER_EVENT_ENTITIES, TASK_MANAGER_EVENT_MODULE } from '../config';
|
|
32
|
-
import { TaskComment } from '../entities';
|
|
33
|
-
import { TaskManagerDataSourceProvider } from './task-manager-datasource.provider';
|
|
34
|
-
export class TaskCommentService extends ApiService {
|
|
35
|
-
async convertSingleDtoToEntity(dto, user) {
|
|
36
|
-
await this.ensureDataSourceRepository();
|
|
37
|
-
let entity = {};
|
|
38
|
-
if ('id' in dto && dto.id) {
|
|
39
|
-
const existing = await this.repository.findOne({
|
|
40
|
-
where: {
|
|
41
|
-
id: dto.id
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
if (!existing) throw new NotFoundException({
|
|
45
|
-
message: 'Comment not found',
|
|
46
|
-
messageKey: TASK_COMMENT_MESSAGES.NOT_FOUND
|
|
47
|
-
});
|
|
48
|
-
if (existing.userId !== user?.id) throw new ForbiddenException({
|
|
49
|
-
message: 'Cannot edit another user comment',
|
|
50
|
-
messageKey: TASK_COMMENT_MESSAGES.FORBIDDEN
|
|
51
|
-
});
|
|
52
|
-
entity = existing;
|
|
53
|
-
}
|
|
54
|
-
const taskId = ('taskId' in dto ? dto.taskId : undefined) ?? entity.taskId;
|
|
55
|
-
return {
|
|
56
|
-
...entity,
|
|
57
|
-
taskId,
|
|
58
|
-
userId: entity.userId !== undefined ? entity.userId : user?.id ?? null,
|
|
59
|
-
content: dto.content,
|
|
60
|
-
parentCommentId: 'parentCommentId' in dto ? dto.parentCommentId ?? null : entity.parentCommentId ?? null
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
async getFilterQuery(query, filter, _user) {
|
|
64
|
-
for (const [key, value] of Object.entries(filter)){
|
|
65
|
-
if (key === 'taskId') {
|
|
66
|
-
query.andWhere('task_comment.taskId = :taskId', {
|
|
67
|
-
taskId: value
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
query,
|
|
73
|
-
isRaw: false
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
constructor(cacheManager, utilsService, dataSourceProvider){
|
|
77
|
-
super(TASK_MANAGER_EVENT_ENTITIES.TASK_COMMENT, cacheManager, utilsService, TaskCommentService.name, false, TASK_MANAGER_EVENT_MODULE, dataSourceProvider, TaskComment), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
TaskCommentService = _ts_decorate([
|
|
81
|
-
Injectable({
|
|
82
|
-
scope: Scope.REQUEST
|
|
83
|
-
}),
|
|
84
|
-
_ts_param(0, Inject('CACHE_INSTANCE')),
|
|
85
|
-
_ts_param(1, Inject(UtilsService)),
|
|
86
|
-
_ts_param(2, Inject(TaskManagerDataSourceProvider)),
|
|
87
|
-
_ts_metadata("design:type", Function),
|
|
88
|
-
_ts_metadata("design:paramtypes", [
|
|
89
|
-
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
90
|
-
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
91
|
-
typeof TaskManagerDataSourceProvider === "undefined" ? Object : TaskManagerDataSourceProvider
|
|
92
|
-
])
|
|
93
|
-
], TaskCommentService);
|