Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
38fb5e5
chore: merge release v3.42.0 back to main [skip ci]
github-actions[bot] May 1, 2026
ee0bccf
feat(pentest): improve scan setup UX
tofikwest May 1, 2026
17d996d
Merge branch 'main' into feature/pentest-scan-ux
tofikwest May 1, 2026
236bf14
Merge pull request #2740 from trycompai/feature/pentest-scan-ux
tofikwest May 1, 2026
6f11e03
fix(pentest): address scan setup review issues
tofikwest May 1, 2026
ab8f4b7
Merge pull request #2743 from trycompai/fix/pentest-scan-review-issues
tofikwest May 1, 2026
96325ab
fix(pentest): forward create notification fields
tofikwest May 2, 2026
ac54500
Merge pull request #2744 from trycompai/fix/pentest-create-notificati…
tofikwest May 2, 2026
2a7552d
feat(documents): add relevance settings (#2745)
github-actions[bot] May 4, 2026
9f9e6a6
fix(integrations): move settings into detail page
tofikwest May 4, 2026
f3c5d57
fix(integrations): preserve empty variable input state
tofikwest May 4, 2026
7bbc643
Merge pull request #2746 from trycompai/fix/integration-detail-settings
tofikwest May 4, 2026
4caa61b
fix(tasks): paginate available integration suggestions
tofikwest May 4, 2026
2631d94
Merge branch 'main' into fix/integration-detail-settings
tofikwest May 4, 2026
d21513e
Merge pull request #2747 from trycompai/fix/integration-detail-settings
tofikwest May 4, 2026
d0150e9
chore(deps): bump the npm_and_yarn group across 2 directories with 2 …
dependabot[bot] May 5, 2026
bc7ee77
fix: move background check toggle to people tab
github-actions[bot] May 5, 2026
54f47d0
feat(api): add per-policy download + reclaim flow for trust access (#…
github-actions[bot] May 5, 2026
78dd8ce
chore: regenerate bun lockfile
tofikwest May 5, 2026
4b5c527
Merge pull request #2750 from trycompai/fix/migration
tofikwest May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"@aws-sdk/s3-request-presigner": "3.1013.0",
"@browserbasehq/sdk": "2.6.0",
"@browserbasehq/stagehand": "^3.2.1",
"@maced/api-client": "^0.9.1",
"@maced/api-client": "^0.9.2",
"@mendable/firecrawl-js": "^4.9.3",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
Expand Down
27 changes: 26 additions & 1 deletion apps/api/src/controls/controls.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ export class ControlsService {
const formTypes = (control.controlDocumentTypes ?? []).map(
(d) => d.formType,
);
const notRelevantSettings =
formTypes.length > 0
? await db.evidenceFormSetting.findMany({
where: {
organizationId,
formType: { in: formTypes },
isNotRelevant: true,
},
select: { formType: true },
})
: [];
const notRelevantFormTypes = new Set(
notRelevantSettings.map((setting) => setting.formType),
);
const submissionCountsByFormType: Record<string, number> = {};
if (formTypes.length > 0) {
const grouped = await db.evidenceSubmission.groupBy({
Expand Down Expand Up @@ -133,6 +147,12 @@ export class ControlsService {

return {
...control,
controlDocumentTypes: (control.controlDocumentTypes ?? []).map(
(documentType) => ({
...documentType,
isNotRelevant: notRelevantFormTypes.has(documentType.formType),
}),
),
submissionCountsByFormType,
progress: {
total: totalItems,
Expand Down Expand Up @@ -309,7 +329,12 @@ export class ControlsService {
// policies. Checking only archivedAt would let user-archived policies
// get re-linked to a control and surface back through the UI.
const policies = await db.policy.findMany({
where: { id: { in: uniqueIds }, organizationId, archivedAt: null, isArchived: false },
where: {
id: { in: uniqueIds },
organizationId,
archivedAt: null,
isArchived: false,
},
select: { id: true },
});
if (policies.length !== uniqueIds.length) {
Expand Down
47 changes: 47 additions & 0 deletions apps/api/src/evidence-forms/evidence-forms.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ jest.mock('../auth/auth.server', () => ({
auth: { api: { getSession: jest.fn() } },
}));

jest.mock('./evidence-forms.service', () => ({
EvidenceFormsService: class EvidenceFormsService {},
}));

jest.mock('../auth/hybrid-auth.guard', () => ({
HybridAuthGuard: class HybridAuthGuard {},
}));

jest.mock('../auth/permission.guard', () => ({
PermissionGuard: class PermissionGuard {},
}));

jest.mock('@trycompai/auth', () => ({
statement: {
evidence: ['create', 'read', 'update', 'delete'],
Expand All @@ -23,6 +35,8 @@ describe('EvidenceFormsController', () => {
const mockService = {
listForms: jest.fn(),
getFormStatuses: jest.fn(),
getFormSettings: jest.fn(),
updateFormSetting: jest.fn(),
getMySubmissions: jest.fn(),
getPendingSubmissionCount: jest.fn(),
getFormWithSubmissions: jest.fn(),
Expand Down Expand Up @@ -88,6 +102,39 @@ describe('EvidenceFormsController', () => {
});
});

describe('getFormSettings', () => {
it('should call service.getFormSettings with organizationId', async () => {
const mockSettings = [{ formType: 'meeting', isNotRelevant: false }];
mockService.getFormSettings.mockResolvedValue(mockSettings);

const result = await controller.getFormSettings('org_1');

expect(result).toEqual(mockSettings);
expect(service.getFormSettings).toHaveBeenCalledWith('org_1');
});
});

describe('updateFormSetting', () => {
it('should call service.updateFormSetting with correct params', async () => {
const mockSetting = { formType: 'meeting', isNotRelevant: true };
const payload = { isNotRelevant: true };
mockService.updateFormSetting.mockResolvedValue(mockSetting);

const result = await controller.updateFormSetting(
'org_1',
'meeting',
payload,
);

expect(result).toEqual(mockSetting);
expect(service.updateFormSetting).toHaveBeenCalledWith({
organizationId: 'org_1',
formType: 'meeting',
payload,
});
});
});

describe('getMySubmissions', () => {
it('should call service.getMySubmissions with correct params', async () => {
const mockSubmissions = [{ id: 'sub_1' }];
Expand Down
30 changes: 30 additions & 0 deletions apps/api/src/evidence-forms/evidence-forms.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,36 @@ export class EvidenceFormsController {
return this.evidenceFormsService.getFormStatuses(organizationId);
}

@Get('settings')
@RequirePermission('evidence', 'read')
@ApiOperation({
summary: 'Get document relevance settings',
description:
'Returns organization-scoped relevance settings for evidence forms',
})
async getFormSettings(@OrganizationId() organizationId: string) {
return this.evidenceFormsService.getFormSettings(organizationId);
}

@Patch(':formType/settings')
@RequirePermission('evidence', 'update')
@ApiOperation({
summary: 'Update document relevance setting',
description:
'Marks an evidence form as relevant or not relevant for the active organization',
})
async updateFormSetting(
@OrganizationId() organizationId: string,
@Param('formType') formType: string,
@Body() body: unknown,
) {
return this.evidenceFormsService.updateFormSetting({
organizationId,
formType,
payload: body,
});
}

@Get('my-submissions')
@RequirePermission('evidence', 'read')
@ApiOperation({
Expand Down
123 changes: 121 additions & 2 deletions apps/api/src/evidence-forms/evidence-forms.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ jest.mock(
{ virtual: true },
);

jest.mock('../frameworks/frameworks-timeline.helper', () => ({
checkAutoCompletePhases: jest.fn(),
}));

jest.mock('../timelines/timelines.service', () => ({
TimelinesService: class TimelinesService {},
}));

jest.mock('@db', () => {
const evidenceFormTypeEnum = {
board_meeting: 'board_meeting',
Expand All @@ -23,24 +31,36 @@ jest.mock('@db', () => {
rbac_matrix: 'rbac_matrix',
infrastructure_inventory: 'infrastructure_inventory',
employee_performance_evaluation: 'employee_performance_evaluation',
network_diagram: 'network_diagram',
tabletop_exercise: 'tabletop_exercise',
};

return {
EvidenceFormType: evidenceFormTypeEnum,
db: {
evidenceSubmission: {
findFirst: jest.fn(),
groupBy: jest.fn(),
update: jest.fn(),
},
evidenceFormSetting: {
findMany: jest.fn(),
upsert: jest.fn(),
},
},
};
});

type MockDb = {
evidenceSubmission: {
findFirst: jest.Mock;
groupBy: jest.Mock;
update: jest.Mock;
};
evidenceFormSetting: {
findMany: jest.Mock;
upsert: jest.Mock;
};
};

describe('EvidenceFormsService', () => {
Expand All @@ -59,15 +79,114 @@ describe('EvidenceFormsService', () => {
getPresignedDownloadUrl: jest.fn(),
} as unknown as AttachmentsService;

const timelinesServiceMock = {} as unknown as import('../timelines/timelines.service').TimelinesService;
const timelinesServiceMock =
{} as unknown as import('../timelines/timelines.service').TimelinesService;

const service = new EvidenceFormsService(attachmentsServiceMock, timelinesServiceMock);
const service = new EvidenceFormsService(
attachmentsServiceMock,
timelinesServiceMock,
);
const mockedDb = db as unknown as MockDb;

beforeEach(() => {
jest.clearAllMocks();
});

describe('getFormStatuses', () => {
it('includes relevance settings alongside latest submission dates', async () => {
mockedDb.evidenceSubmission.groupBy.mockResolvedValue([
{
formType: 'meeting',
_max: { submittedAt: new Date('2026-01-01T00:00:00.000Z') },
},
]);
mockedDb.evidenceFormSetting.findMany.mockResolvedValue([
{ formType: 'meeting', isNotRelevant: true },
]);

const result = await service.getFormStatuses('org_123');

expect(mockedDb.evidenceFormSetting.findMany).toHaveBeenCalledWith({
where: { organizationId: 'org_123' },
select: { formType: true, isNotRelevant: true },
});
expect(result.meeting).toEqual({
lastSubmittedAt: '2026-01-01T00:00:00.000Z',
isNotRelevant: true,
});
expect(result['access-request']).toEqual({
lastSubmittedAt: null,
isNotRelevant: false,
});
});
});

describe('getFormSettings', () => {
it('returns one setting entry for every document form', async () => {
mockedDb.evidenceFormSetting.findMany.mockResolvedValue([
{
formType: 'access_request',
isNotRelevant: true,
updatedAt: new Date('2026-01-02T00:00:00.000Z'),
},
]);

const result = await service.getFormSettings('org_123');

expect(result).toContainEqual({
formType: 'access-request',
isNotRelevant: true,
updatedAt: '2026-01-02T00:00:00.000Z',
});
expect(result).toContainEqual({
formType: 'meeting',
isNotRelevant: false,
updatedAt: null,
});
});
});

describe('updateFormSetting', () => {
it('upserts a document relevance setting', async () => {
mockedDb.evidenceFormSetting.upsert.mockResolvedValue({
formType: 'meeting',
isNotRelevant: true,
updatedAt: new Date('2026-01-03T00:00:00.000Z'),
});

const result = await service.updateFormSetting({
organizationId: 'org_123',
formType: 'meeting',
payload: { isNotRelevant: true },
});

expect(mockedDb.evidenceFormSetting.upsert).toHaveBeenCalledWith({
where: {
organizationId_formType: {
organizationId: 'org_123',
formType: 'meeting',
},
},
create: {
organizationId: 'org_123',
formType: 'meeting',
isNotRelevant: true,
},
update: { isNotRelevant: true },
select: {
formType: true,
isNotRelevant: true,
updatedAt: true,
},
});
expect(result).toEqual({
formType: 'meeting',
isNotRelevant: true,
updatedAt: '2026-01-03T00:00:00.000Z',
});
});
});

describe('reviewSubmission', () => {
it('includes submittedBy and reviewedBy relations on review update', async () => {
mockedDb.evidenceSubmission.findFirst.mockResolvedValue({
Expand Down
Loading
Loading