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
a81af3e
feat(frameworks): add custom framework and requirement management end…
Apr 17, 2026
c39a7c5
fix(frameworks): dedupe linked requirements and scope lookups by org
Apr 17, 2026
5f85389
Merge branch 'main' into lewis/comp-framework-controls
carhartlewis Apr 17, 2026
c3a50db
fix(onboarding): set default value for organizationId in updatePolicy…
carhartlewis Apr 17, 2026
0380ff7
feat(controls): add endpoints to link policies, tasks, and requiremen…
carhartlewis Apr 17, 2026
f40226a
Merge branch 'main' into lewis/comp-framework-controls
carhartlewis Apr 17, 2026
9f1a8cc
refactor(frameworks): split custom frameworks into dedicated per-org …
carhartlewis Apr 17, 2026
409be26
fix(frameworks): remove debug log and return DB-reported link counts
carhartlewis Apr 17, 2026
2b8991f
fix(controls): scope FK inputs to caller org in create()
carhartlewis Apr 17, 2026
d296e2a
fix(frameworks): address review feedback — tenant-FK hardening + cust…
carhartlewis Apr 17, 2026
5de8a67
fix(controls): dedupe policyIds/taskIds before validating length
carhartlewis Apr 17, 2026
78f36f0
Merge branch 'main' into lewis/comp-framework-controls
carhartlewis Apr 18, 2026
b4f6cf6
chore: merge release v3.24.0 back to main [skip ci]
github-actions[bot] Apr 19, 2026
4f6f72a
chore: merge main into lewis/comp-framework-controls
carhartlewis Apr 20, 2026
2ffc0bd
Merge branch 'main' into lewis/comp-framework-controls
carhartlewis Apr 20, 2026
e432288
Merge pull request #2593 from trycompai/lewis/comp-framework-controls
carhartlewis Apr 20, 2026
e3b58bc
fix(frameworks): address PR review feedback
carhartlewis Apr 20, 2026
e4fbb8c
Merge pull request #2602 from trycompai/lewis/comp-framework-controls…
carhartlewis Apr 20, 2026
3f681e5
fix(framework-editor): stop auto-linking all framework requirements o…
github-actions[bot] Apr 20, 2026
a7389a1
fix(app): hide policy alerts if pending version is cleared (#2604)
github-actions[bot] Apr 20, 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
43 changes: 32 additions & 11 deletions apps/api/src/admin-organizations/admin-policies.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,41 @@ export class AdminPoliciesController {
) {
const instances = await db.frameworkInstance.findMany({
where: { organizationId: orgId },
include: { framework: true },
include: { framework: true, customFramework: true },
});

const normalized = instances.map((fi) => {
if (fi.framework) {
return {
id: fi.framework.id,
name: fi.framework.name,
version: fi.framework.version,
description: fi.framework.description,
visible: fi.framework.visible,
createdAt: fi.framework.createdAt,
updatedAt: fi.framework.updatedAt,
};
}
if (fi.customFramework) {
return {
id: fi.customFramework.id,
name: fi.customFramework.name,
version: fi.customFramework.version,
description: fi.customFramework.description,
visible: true,
createdAt: fi.customFramework.createdAt,
updatedAt: fi.customFramework.updatedAt,
};
}
return null;
});
const uniqueFrameworks = Array.from(
new Map(instances.map((fi) => [fi.framework.id, fi.framework])).values(),
).map((f) => ({
id: f.id,
name: f.name,
version: f.version,
description: f.description,
visible: f.visible,
createdAt: f.createdAt,
updatedAt: f.updatedAt,
}));
new Map(
normalized
.filter((f): f is NonNullable<typeof f> => f !== null)
.map((f) => [f.id, f]),
).values(),
);

const contextEntries = await db.context.findMany({
where: { organizationId: orgId },
Expand Down
78 changes: 78 additions & 0 deletions apps/api/src/controls/controls.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Delete,
Get,
Param,
ParseEnumPipe,
Post,
Query,
UseGuards,
Expand All @@ -20,6 +21,11 @@ import { RequirePermission } from '../auth/require-permission.decorator';
import { OrganizationId } from '../auth/auth-context.decorator';
import { ControlsService } from './controls.service';
import { CreateControlDto } from './dto/create-control.dto';
import { LinkPoliciesDto } from './dto/link-policies.dto';
import { LinkTasksDto } from './dto/link-tasks.dto';
import { LinkRequirementsToControlDto } from './dto/link-requirements.dto';
import { LinkDocumentTypesDto } from './dto/link-document-types.dto';
import { EvidenceFormType } from '@db';

@ApiTags('Controls')
@ApiBearerAuth()
Expand Down Expand Up @@ -92,6 +98,78 @@ export class ControlsController {
return this.controlsService.create(organizationId, dto);
}

@Post(':id/policies/link')
@RequirePermission('control', 'update')
@ApiOperation({ summary: 'Link existing policies to a control' })
async linkPolicies(
@OrganizationId() organizationId: string,
@Param('id') id: string,
@Body() dto: LinkPoliciesDto,
) {
return this.controlsService.linkPolicies(
id,
organizationId,
dto.policyIds,
);
}

@Post(':id/tasks/link')
@RequirePermission('control', 'update')
@ApiOperation({ summary: 'Link existing tasks to a control' })
async linkTasks(
@OrganizationId() organizationId: string,
@Param('id') id: string,
@Body() dto: LinkTasksDto,
) {
return this.controlsService.linkTasks(id, organizationId, dto.taskIds);
}

@Post(':id/requirements/link')
@RequirePermission('control', 'update')
@ApiOperation({ summary: 'Link existing requirements to a control' })
async linkRequirements(
@OrganizationId() organizationId: string,
@Param('id') id: string,
@Body() dto: LinkRequirementsToControlDto,
) {
return this.controlsService.linkRequirements(
id,
organizationId,
dto.requirements,
);
}

@Post(':id/document-types/link')
@RequirePermission('control', 'update')
@ApiOperation({ summary: 'Link required document types to a control' })
async linkDocumentTypes(
@OrganizationId() organizationId: string,
@Param('id') id: string,
@Body() dto: LinkDocumentTypesDto,
) {
return this.controlsService.linkDocumentTypes(
id,
organizationId,
dto.formTypes,
);
}

@Delete(':id/document-types/:formType')
@RequirePermission('control', 'update')
@ApiOperation({ summary: 'Remove a required document type from a control' })
async unlinkDocumentType(
@OrganizationId() organizationId: string,
@Param('id') id: string,
@Param('formType', new ParseEnumPipe(EvidenceFormType))
formType: EvidenceFormType,
) {
return this.controlsService.unlinkDocumentType(
id,
organizationId,
formType,
);
}

@Delete(':id')
@RequirePermission('control', 'delete')
@ApiOperation({ summary: 'Delete a control' })
Expand Down
Loading
Loading