Skip to content

Commit 5216a50

Browse files
authored
Merge branch 'main' into fix/custom-role-rbac-bugs
2 parents 568dd56 + 48830d8 commit 5216a50

41 files changed

Lines changed: 2125 additions & 336 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/api/src/controls/controls.controller.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,9 @@ export class ControlsController {
8484
async findOne(
8585
@OrganizationId() organizationId: string,
8686
@Param('id') id: string,
87+
@Query('frameworkInstanceId') frameworkInstanceId?: string,
8788
) {
88-
return this.controlsService.findOne(id, organizationId);
89+
return this.controlsService.findOne(id, organizationId, frameworkInstanceId);
8990
}
9091

9192
@Post()
@@ -105,11 +106,13 @@ export class ControlsController {
105106
@OrganizationId() organizationId: string,
106107
@Param('id') id: string,
107108
@Body() dto: LinkPoliciesDto,
109+
@Query('frameworkInstanceId') frameworkInstanceId?: string,
108110
) {
109111
return this.controlsService.linkPolicies(
110112
id,
111113
organizationId,
112114
dto.policyIds,
115+
frameworkInstanceId,
113116
);
114117
}
115118

@@ -120,8 +123,14 @@ export class ControlsController {
120123
@OrganizationId() organizationId: string,
121124
@Param('id') id: string,
122125
@Body() dto: LinkTasksDto,
126+
@Query('frameworkInstanceId') frameworkInstanceId?: string,
123127
) {
124-
return this.controlsService.linkTasks(id, organizationId, dto.taskIds);
128+
return this.controlsService.linkTasks(
129+
id,
130+
organizationId,
131+
dto.taskIds,
132+
frameworkInstanceId,
133+
);
125134
}
126135

127136
@Post(':id/requirements/link')
@@ -146,11 +155,13 @@ export class ControlsController {
146155
@OrganizationId() organizationId: string,
147156
@Param('id') id: string,
148157
@Body() dto: LinkDocumentTypesDto,
158+
@Query('frameworkInstanceId') frameworkInstanceId?: string,
149159
) {
150160
return this.controlsService.linkDocumentTypes(
151161
id,
152162
organizationId,
153163
dto.formTypes,
164+
frameworkInstanceId,
154165
);
155166
}
156167

@@ -162,11 +173,13 @@ export class ControlsController {
162173
@Param('id') id: string,
163174
@Param('formType', new ParseEnumPipe(EvidenceFormType))
164175
formType: EvidenceFormType,
176+
@Query('frameworkInstanceId') frameworkInstanceId?: string,
165177
) {
166178
return this.controlsService.unlinkDocumentType(
167179
id,
168180
organizationId,
169181
formType,
182+
frameworkInstanceId,
170183
);
171184
}
172185

apps/api/src/controls/controls.service.ts

Lines changed: 198 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,15 @@ export class ControlsService {
9393
};
9494
}
9595

96-
async findOne(controlId: string, organizationId: string) {
96+
async findOne(
97+
controlId: string,
98+
organizationId: string,
99+
frameworkInstanceId?: string,
100+
) {
101+
if (frameworkInstanceId) {
102+
return this.findOneForFramework(controlId, organizationId, frameworkInstanceId);
103+
}
104+
97105
const control = await db.control.findUnique({
98106
where: { id: controlId, organizationId },
99107
include: {
@@ -117,7 +125,11 @@ export class ControlsService {
117125
throw new NotFoundException('Control not found');
118126
}
119127

120-
const formTypes = (control.controlDocumentTypes ?? []).map(
128+
const policies = control.policies || [];
129+
const tasks = control.tasks || [];
130+
const controlDocumentTypes = control.controlDocumentTypes || [];
131+
132+
const formTypes = controlDocumentTypes.map(
121133
(d) => d.formType,
122134
);
123135
const notRelevantSettings =
@@ -150,8 +162,6 @@ export class ControlsService {
150162
}
151163

152164
// Compute progress
153-
const policies = control.policies || [];
154-
const tasks = control.tasks || [];
155165
const totalItems = policies.length + tasks.length;
156166

157167
let policyCompleted = 0;
@@ -168,7 +178,9 @@ export class ControlsService {
168178

169179
return {
170180
...control,
171-
controlDocumentTypes: (control.controlDocumentTypes ?? []).map(
181+
policies,
182+
tasks,
183+
controlDocumentTypes: controlDocumentTypes.map(
172184
(documentType) => ({
173185
...documentType,
174186
isNotRelevant: notRelevantFormTypes.has(documentType.formType),
@@ -188,6 +200,118 @@ export class ControlsService {
188200
};
189201
}
190202

203+
private async findOneForFramework(
204+
controlId: string,
205+
organizationId: string,
206+
frameworkInstanceId: string,
207+
) {
208+
await this.ensureFrameworkInstance(frameworkInstanceId, organizationId);
209+
const control = await db.control.findUnique({
210+
where: { id: controlId, organizationId },
211+
include: {
212+
frameworkPolicyLinks: {
213+
where: {
214+
frameworkInstanceId,
215+
policy: { archivedAt: null },
216+
},
217+
include: { policy: true },
218+
},
219+
frameworkTaskLinks: {
220+
where: {
221+
frameworkInstanceId,
222+
task: { archivedAt: null },
223+
},
224+
include: { task: true },
225+
},
226+
frameworkDocumentLinks: {
227+
where: { frameworkInstanceId },
228+
},
229+
requirementsMapped: {
230+
where: { archivedAt: null },
231+
include: {
232+
frameworkInstance: {
233+
include: { framework: true, customFramework: true },
234+
},
235+
requirement: true,
236+
customRequirement: true,
237+
},
238+
},
239+
},
240+
});
241+
242+
if (!control) {
243+
throw new NotFoundException('Control not found');
244+
}
245+
246+
const policies = control.frameworkPolicyLinks.map((link) => link.policy);
247+
const tasks = control.frameworkTaskLinks.map((link) => link.task);
248+
const controlDocumentTypes = control.frameworkDocumentLinks;
249+
const formTypes = controlDocumentTypes.map((d) => d.formType);
250+
const notRelevantSettings =
251+
formTypes.length > 0
252+
? await db.evidenceFormSetting.findMany({
253+
where: {
254+
organizationId,
255+
formType: { in: formTypes },
256+
isNotRelevant: true,
257+
},
258+
select: { formType: true },
259+
})
260+
: [];
261+
const notRelevantFormTypes = new Set(
262+
notRelevantSettings.map((setting) => setting.formType),
263+
);
264+
const submissionCountsByFormType: Record<string, number> = {};
265+
if (formTypes.length > 0) {
266+
const grouped = await db.evidenceSubmission.groupBy({
267+
by: ['formType'],
268+
where: {
269+
organizationId,
270+
formType: { in: formTypes },
271+
},
272+
_count: { _all: true },
273+
});
274+
for (const g of grouped) {
275+
submissionCountsByFormType[g.formType] = g._count._all;
276+
}
277+
}
278+
279+
const policyCompleted = policies.filter((p) => p.status === 'published').length;
280+
const taskCompleted = tasks.filter(
281+
(t) => t.status === 'done' || t.status === 'not_relevant',
282+
).length;
283+
const completed = policyCompleted + taskCompleted;
284+
const totalItems = policies.length + tasks.length;
285+
286+
const {
287+
frameworkPolicyLinks,
288+
frameworkTaskLinks,
289+
frameworkDocumentLinks,
290+
...controlData
291+
} = control;
292+
293+
return {
294+
...controlData,
295+
policies,
296+
tasks,
297+
controlDocumentTypes: controlDocumentTypes.map((documentType) => ({
298+
...documentType,
299+
isNotRelevant: notRelevantFormTypes.has(documentType.formType),
300+
})),
301+
submissionCountsByFormType,
302+
progress: {
303+
total: totalItems,
304+
completed,
305+
progress:
306+
totalItems > 0 ? Math.round((completed / totalItems) * 100) : 0,
307+
byType: {
308+
policy: { total: policies.length, completed: policyCompleted },
309+
task: { total: tasks.length, completed: taskCompleted },
310+
},
311+
},
312+
};
313+
}
314+
191315
async getOptions(organizationId: string) {
192316
const [policies, tasks, frameworkInstances] = await Promise.all([
193317
db.policy.findMany({
@@ -480,10 +604,25 @@ export class ControlsService {
480604
return control;
481605
}
482606

607+
private async ensureFrameworkInstance(
608+
frameworkInstanceId: string,
609+
organizationId: string,
610+
) {
611+
const frameworkInstance = await db.frameworkInstance.findUnique({
612+
where: { id: frameworkInstanceId, organizationId },
613+
select: { id: true },
614+
});
615+
if (!frameworkInstance) {
616+
throw new NotFoundException('Framework instance not found');
617+
}
618+
return frameworkInstance;
619+
}
620+
483621
async linkPolicies(
484622
controlId: string,
485623
organizationId: string,
486624
policyIds: string[],
625+
frameworkInstanceId?: string,
487626
) {
488627
await this.ensureControl(controlId, organizationId);
489628

@@ -495,10 +634,22 @@ export class ControlsService {
495634
throw new BadRequestException('No valid policies to link');
496635
}
497636

498-
await db.control.update({
499-
where: { id: controlId },
500-
data: { policies: { connect: policies.map((p) => ({ id: p.id })) } },
501-
});
637+
if (frameworkInstanceId) {
638+
await this.ensureFrameworkInstance(frameworkInstanceId, organizationId);
639+
await db.frameworkControlPolicyLink.createMany({
640+
data: policies.map((policy) => ({
641+
frameworkInstanceId,
642+
controlId,
643+
policyId: policy.id,
644+
})),
645+
skipDuplicates: true,
646+
});
647+
} else {
648+
await db.control.update({
649+
where: { id: controlId },
650+
data: { policies: { connect: policies.map((p) => ({ id: p.id })) } },
651+
});
652+
}
502653

503654
return { count: policies.length };
504655
}
@@ -507,6 +658,7 @@ export class ControlsService {
507658
controlId: string,
508659
organizationId: string,
509660
taskIds: string[],
661+
frameworkInstanceId?: string,
510662
) {
511663
await this.ensureControl(controlId, organizationId);
512664

@@ -518,10 +670,22 @@ export class ControlsService {
518670
throw new BadRequestException('No valid tasks to link');
519671
}
520672

521-
await db.control.update({
522-
where: { id: controlId },
523-
data: { tasks: { connect: tasks.map((t) => ({ id: t.id })) } },
524-
});
673+
if (frameworkInstanceId) {
674+
await this.ensureFrameworkInstance(frameworkInstanceId, organizationId);
675+
await db.frameworkControlTaskLink.createMany({
676+
data: tasks.map((task) => ({
677+
frameworkInstanceId,
678+
controlId,
679+
taskId: task.id,
680+
})),
681+
skipDuplicates: true,
682+
});
683+
} else {
684+
await db.control.update({
685+
where: { id: controlId },
686+
data: { tasks: { connect: tasks.map((t) => ({ id: t.id })) } },
687+
});
688+
}
525689

526690
return { count: tasks.length };
527691
}
@@ -627,8 +791,21 @@ export class ControlsService {
627791
controlId: string,
628792
organizationId: string,
629793
formTypes: EvidenceFormType[],
794+
frameworkInstanceId?: string,
630795
) {
631796
await this.ensureControl(controlId, organizationId);
797+
if (frameworkInstanceId) {
798+
await this.ensureFrameworkInstance(frameworkInstanceId, organizationId);
799+
const result = await db.frameworkControlDocumentTypeLink.createMany({
800+
data: formTypes.map((formType) => ({
801+
frameworkInstanceId,
802+
controlId,
803+
formType,
804+
})),
805+
skipDuplicates: true,
806+
});
807+
return { count: result.count };
808+
}
632809
const result = await db.controlDocumentType.createMany({
633810
data: formTypes.map((formType) => ({ controlId, formType })),
634811
skipDuplicates: true,
@@ -640,8 +817,16 @@ export class ControlsService {
640817
controlId: string,
641818
organizationId: string,
642819
formType: EvidenceFormType,
820+
frameworkInstanceId?: string,
643821
) {
644822
await this.ensureControl(controlId, organizationId);
823+
if (frameworkInstanceId) {
824+
await this.ensureFrameworkInstance(frameworkInstanceId, organizationId);
825+
await db.frameworkControlDocumentTypeLink.deleteMany({
826+
where: { frameworkInstanceId, controlId, formType },
827+
});
828+
return { success: true };
829+
}
645830
await db.controlDocumentType.deleteMany({
646831
where: { controlId, formType },
647832
});

0 commit comments

Comments
 (0)