Skip to content

Commit b0a6545

Browse files
authored
Merge pull request #2791 from trycompai/main
[comp] Production Deploy
2 parents 8685fa1 + ea226b0 commit b0a6545

15 files changed

Lines changed: 480 additions & 67 deletions

File tree

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

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@ import {
66
import { db, EvidenceFormType, Prisma } from '@db';
77
import { CreateControlDto } from './dto/create-control.dto';
88

9+
// A CustomRequirement is valid for a given FrameworkInstance when its parent
10+
// matches: either it lives on the FI's CustomFramework, or it was attached
11+
// directly to the FI itself (per-instance custom requirement on a platform
12+
// framework). The CustomRequirement schema's CHECK enforces that exactly one
13+
// of customFrameworkId / frameworkInstanceId is set.
14+
function isCustomReqOnInstance(
15+
req: {
16+
customFrameworkId: string | null;
17+
frameworkInstanceId: string | null;
18+
},
19+
instance: { id: string; customFrameworkId: string | null },
20+
): boolean {
21+
if (req.customFrameworkId) {
22+
return (
23+
instance.customFrameworkId !== null &&
24+
req.customFrameworkId === instance.customFrameworkId
25+
);
26+
}
27+
return req.frameworkInstanceId === instance.id;
28+
}
29+
930
const controlInclude = {
1031
policies: {
1132
where: { archivedAt: null },
@@ -402,16 +423,24 @@ export class ControlsService {
402423
customReqIds.length > 0
403424
? db.customRequirement.findMany({
404425
where: { id: { in: customReqIds }, organizationId },
405-
select: { id: true, customFrameworkId: true },
426+
select: {
427+
id: true,
428+
customFrameworkId: true,
429+
frameworkInstanceId: true,
430+
},
406431
})
407-
: Promise.resolve<{ id: string; customFrameworkId: string }[]>([]),
432+
: Promise.resolve<
433+
{
434+
id: string;
435+
customFrameworkId: string | null;
436+
frameworkInstanceId: string | null;
437+
}[]
438+
>([]),
408439
]);
409440
const platformReqFwById = new Map(
410441
platformReqs.map((r) => [r.id, r.frameworkId]),
411442
);
412-
const customReqFwById = new Map(
413-
customReqs.map((r) => [r.id, r.customFrameworkId]),
414-
);
443+
const customReqById = new Map(customReqs.map((r) => [r.id, r]));
415444

416445
for (const m of mappings) {
417446
const instance = instanceById.get(m.frameworkInstanceId);
@@ -428,8 +457,8 @@ export class ControlsService {
428457
);
429458
}
430459
} else if (m.customRequirementId) {
431-
const reqFwId = customReqFwById.get(m.customRequirementId);
432-
if (!reqFwId || reqFwId !== instance.customFrameworkId) {
460+
const req = customReqById.get(m.customRequirementId);
461+
if (!req || !isCustomReqOnInstance(req, instance)) {
433462
throw new BadRequestException(
434463
'One or more requirement mappings are invalid',
435464
);
@@ -544,16 +573,24 @@ export class ControlsService {
544573
customReqIds.length > 0
545574
? db.customRequirement.findMany({
546575
where: { id: { in: customReqIds }, organizationId },
547-
select: { id: true, customFrameworkId: true },
576+
select: {
577+
id: true,
578+
customFrameworkId: true,
579+
frameworkInstanceId: true,
580+
},
548581
})
549-
: Promise.resolve<{ id: string; customFrameworkId: string }[]>([]),
582+
: Promise.resolve<
583+
{
584+
id: string;
585+
customFrameworkId: string | null;
586+
frameworkInstanceId: string | null;
587+
}[]
588+
>([]),
550589
]);
551590
const platformReqFwById = new Map(
552591
platformReqs.map((r) => [r.id, r.frameworkId]),
553592
);
554-
const customReqFwById = new Map(
555-
customReqs.map((r) => [r.id, r.customFrameworkId]),
556-
);
593+
const customReqById = new Map(customReqs.map((r) => [r.id, r]));
557594

558595
const validMappings = mappings.filter((m) => {
559596
const instance = instanceById.get(m.frameworkInstanceId);
@@ -563,8 +600,8 @@ export class ControlsService {
563600
return Boolean(reqFwId) && reqFwId === instance.frameworkId;
564601
}
565602
if (m.customRequirementId) {
566-
const reqFwId = customReqFwById.get(m.customRequirementId);
567-
return Boolean(reqFwId) && reqFwId === instance.customFrameworkId;
603+
const req = customReqById.get(m.customRequirementId);
604+
return Boolean(req) && isCustomReqOnInstance(req!, instance);
568605
}
569606
return false;
570607
});

apps/api/src/frameworks/frameworks.controller.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ export class FrameworksController {
8585
return this.frameworksService.getScores(organizationId, authContext.userId);
8686
}
8787

88+
@Get('update-statuses')
89+
@RequirePermission('framework', 'read')
90+
@ApiOperation({ summary: 'Get update statuses for all framework instances' })
91+
async getAllUpdateStatuses(@OrganizationId() organizationId: string) {
92+
const data =
93+
await this.frameworksService.getAllUpdateStatuses(organizationId);
94+
return { data, count: data.length };
95+
}
96+
8897
@Get(':id')
8998
@RequirePermission('framework', 'read')
9099
@ApiOperation({ summary: 'Get a single framework instance with full detail' })

apps/api/src/frameworks/frameworks.service.spec.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { NotFoundException } from '@nestjs/common';
33
import { FrameworksService } from './frameworks.service';
4+
import { TimelinesService } from '../timelines/timelines.service';
45

56
jest.mock('@db', () => ({
67
db: {
@@ -17,6 +18,7 @@ jest.mock('@db', () => ({
1718
findMany: jest.fn(),
1819
findFirst: jest.fn(),
1920
create: jest.fn(),
21+
createManyAndReturn: jest.fn(),
2022
},
2123
requirementMap: {
2224
findMany: jest.fn(),
@@ -29,6 +31,9 @@ jest.mock('@db', () => ({
2931
findMany: jest.fn(),
3032
},
3133
},
34+
// The frameworks-timeline helper imports FindingType (a Prisma enum) at module
35+
// load. Stub it so the spec file can be evaluated without the real client.
36+
FindingType: { soc2: 'soc2', iso27001: 'iso27001' },
3237
}));
3338

3439
jest.mock('./frameworks-scores.helper', () => ({
@@ -50,7 +55,10 @@ describe('FrameworksService', () => {
5055

5156
beforeEach(async () => {
5257
const module: TestingModule = await Test.createTestingModule({
53-
providers: [FrameworksService],
58+
providers: [
59+
FrameworksService,
60+
{ provide: TimelinesService, useValue: {} },
61+
],
5462
}).compile();
5563

5664
service = module.get<FrameworksService>(FrameworksService);
@@ -192,12 +200,13 @@ describe('FrameworksService', () => {
192200
expect(result.requirementDefinitions).toHaveLength(1);
193201
});
194202

195-
it('findOne on a platform FI reads only FrameworkEditorRequirement', async () => {
203+
it('findOne on a platform FI merges per-instance custom requirements with platform requirements', async () => {
196204
(mockDb.frameworkInstance.findUnique as jest.Mock).mockResolvedValue({
197205
id: 'fi_platform',
198206
organizationId: 'org_A',
199207
frameworkId: 'frk_soc2',
200208
customFrameworkId: null,
209+
currentVersionId: null,
201210
framework: { id: 'frk_soc2', name: 'SOC 2' },
202211
customFramework: null,
203212
requirementsMapped: [],
@@ -207,32 +216,83 @@ describe('FrameworksService', () => {
207216
).mockResolvedValue([
208217
{ id: 'frk_rq_1', name: 'CC1', identifier: 'cc1-1', description: '' },
209218
]);
219+
// Per-instance custom requirement attached directly to fi_platform.
220+
(mockDb.customRequirement.findMany as jest.Mock).mockResolvedValue([
221+
{
222+
id: 'creq_local',
223+
name: 'Org-local extra',
224+
identifier: 'X1',
225+
description: '',
226+
},
227+
]);
210228
(mockDb.task.findMany as jest.Mock).mockResolvedValue([]);
211229
(mockDb.requirementMap.findMany as jest.Mock).mockResolvedValue([]);
212230
(mockDb.evidenceSubmission.findMany as jest.Mock).mockResolvedValue([]);
213231

214-
await service.findOne('fi_platform', 'org_A');
232+
const result = await service.findOne('fi_platform', 'org_A');
215233

216234
expect(mockDb.frameworkEditorRequirement.findMany).toHaveBeenCalledWith({
217235
where: { frameworkId: 'frk_soc2' },
218236
orderBy: { name: 'asc' },
219237
});
220-
expect(mockDb.customRequirement.findMany).not.toHaveBeenCalled();
238+
expect(mockDb.customRequirement.findMany).toHaveBeenCalledWith({
239+
where: { frameworkInstanceId: 'fi_platform' },
240+
orderBy: { name: 'asc' },
241+
});
242+
const ids = result.requirementDefinitions.map((r: any) => r.id);
243+
expect(ids).toEqual(expect.arrayContaining(['frk_rq_1', 'creq_local']));
244+
});
245+
246+
it('createRequirement on a custom-framework FI hangs the row off the framework', async () => {
247+
(mockDb.frameworkInstance.findUnique as jest.Mock).mockResolvedValue({
248+
id: 'fi_custom',
249+
customFrameworkId: 'cfrm_A',
250+
});
251+
(mockDb.customRequirement.create as jest.Mock).mockResolvedValue({
252+
id: 'creq_new',
253+
});
254+
255+
await service.createRequirement('fi_custom', 'org_A', {
256+
name: 'x',
257+
identifier: 'x',
258+
description: 'x',
259+
});
260+
261+
expect(mockDb.customRequirement.create).toHaveBeenCalledWith({
262+
data: {
263+
name: 'x',
264+
identifier: 'x',
265+
description: 'x',
266+
organizationId: 'org_A',
267+
customFrameworkId: 'cfrm_A',
268+
},
269+
});
221270
});
222271

223-
it('createRequirement rejects a platform framework instance', async () => {
272+
it('createRequirement on a platform FI hangs the row off the instance', async () => {
224273
(mockDb.frameworkInstance.findUnique as jest.Mock).mockResolvedValue({
274+
id: 'fi_platform',
225275
customFrameworkId: null,
226276
});
277+
(mockDb.customRequirement.create as jest.Mock).mockResolvedValue({
278+
id: 'creq_new',
279+
});
227280

228-
await expect(
229-
service.createRequirement('fi_platform', 'org_A', {
281+
await service.createRequirement('fi_platform', 'org_A', {
282+
name: 'x',
283+
identifier: 'x',
284+
description: 'x',
285+
});
286+
287+
expect(mockDb.customRequirement.create).toHaveBeenCalledWith({
288+
data: {
230289
name: 'x',
231290
identifier: 'x',
232291
description: 'x',
233-
}),
234-
).rejects.toThrow(/Cannot add custom requirements/);
235-
expect(mockDb.customRequirement.create).not.toHaveBeenCalled();
292+
organizationId: 'org_A',
293+
frameworkInstanceId: 'fi_platform',
294+
},
295+
});
236296
});
237297
});
238298
});

0 commit comments

Comments
 (0)