Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions apps/app/languine.lock
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ files:
editor.ai_selector.replace: 05fdc089c4c320832588a69475ff5e79
editor.ai_selector.insert: 3005820694a4944d91c4c4e8b8a016d3
editor.ai_selector.discard: d94b42030b9785fd754d5c1754961269
evidence.title: 63b1eb1a39194d82271ec937fba19714
evidence.description: c8fcd9bc15a38dcd7f6d0cbdeb65f33c
src/locales/es.ts:
language.title: c88b33a3250b4e28f34e1ececba69206
language.description: b74d060770289b75ba34d38ea1e05251
Expand Down
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@ai-sdk/openai": "^1.1.12",
"@ai-sdk/provider": "^1.0.7",
"@aws-sdk/s3-request-presigner": "^3.750.0",
"@browserbasehq/sdk": "^2.3.0",
"@bubba/notifications": "workspace:*",
"@date-fns/tz": "^1.2.0",
Expand Down
36 changes: 31 additions & 5 deletions apps/app/src/actions/framework/select-frameworks-action.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use server";

import { db, type Policy, type User } from "@bubba/db";
import { db, RequirementType, type Policy, type User } from "@bubba/db";
import { authActionClient } from "../safe-action";
import { z } from "zod";
import type { ActionData } from "../types";
Expand Down Expand Up @@ -30,25 +30,28 @@ export const selectFrameworksAction = authActionClient
}

try {
// First create categories
// Create categories
await createOrganizationCategories(user as User, frameworkIds);

// Then create frameworks and controls
// Create frameworks and controls
const organizationFrameworks = await Promise.all(
frameworkIds.map((frameworkId) =>
createOrganizationFramework(user as User, frameworkId)
)
);

// Finally create policies
// Create policies
await createOrganizationPolicy(user as User, frameworkIds);

// Finally create control requirements
// Create control requirements
await createOrganizationControlRequirements(
user as User,
organizationFrameworks.map((framework) => framework.id)
);

// Create organization evidence
await createOrganizationEvidence(user as User);

Comment on lines +52 to +54
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider handling partial failures in framework selection.

The evidence creation step could fail independently of other steps. Consider implementing a rollback mechanism or handling partial failures.


🏁 Script executed:

#!/bin/bash
# Description: Check for error handling patterns in similar server actions

# Search for transaction usage in server actions
rg -A 5 '\$transaction' --type ts

Length of output: 3199


Action Required: Enhance Failure Handling for Evidence Creation

  • In apps/app/src/actions/framework/select-frameworks-action.ts (lines 52–54), the current implementation simply awaits createOrganizationEvidence(user as User) without protecting the operation within a transactional block or explicit error handling.
  • Similar server actions in the codebase are wrapped in a db.$transaction to ensure rollback on failures. Consider adopting a similar approach here or implementing dedicated error handling to manage potential partial failures during framework selection.

return {
data: true,
};
Expand Down Expand Up @@ -244,3 +247,26 @@ const createOrganizationControlRequirements = async (

return controlRequirements;
};

const createOrganizationEvidence = async (user: User) => {
if (!user.organizationId) {
throw new Error("Not authorized - no organization found");
}

const evidence = await db.controlRequirement.findMany({
where: {
type: RequirementType.evidence,
},
});

const organizationEvidence = await db.organizationEvidence.createMany({
data: evidence.map((evidence) => ({
organizationId: user.organizationId!,
evidenceId: evidence.id,
name: evidence.name,
description: evidence.description,
})),
});

return organizationEvidence;
};
Comment on lines +251 to +272
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize database operations in createOrganizationEvidence.

The function could be optimized to:

  1. Add error handling for database operations
  2. Use transaction for atomic operations
  3. Add batch size limit for large datasets
 const createOrganizationEvidence = async (user: User) => {
   if (!user.organizationId) {
     throw new Error("Not authorized - no organization found");
   }

+  const BATCH_SIZE = 1000;
+
+  try {
+    return await db.$transaction(async (tx) => {
       const evidence = await tx.controlRequirement.findMany({
         where: {
           type: RequirementType.evidence,
         },
       });
+
+      const chunks = [];
+      for (let i = 0; i < evidence.length; i += BATCH_SIZE) {
+        chunks.push(evidence.slice(i, i + BATCH_SIZE));
+      }
+
+      const results = await Promise.all(
+        chunks.map((chunk) =>
+          tx.organizationEvidence.createMany({
+            data: chunk.map((evidence) => ({
+              organizationId: user.organizationId!,
+              evidenceId: evidence.id,
+              name: evidence.name,
+              description: evidence.description,
+            })),
+          })
+        )
+      );
+
+      return results.reduce(
+        (acc, result) => ({
+          count: acc.count + result.count,
+        }),
+        { count: 0 }
+      );
+    });
+  } catch (error) {
+    console.error("Error creating organization evidence:", error);
+    throw new Error("Failed to create organization evidence");
+  }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const createOrganizationEvidence = async (user: User) => {
if (!user.organizationId) {
throw new Error("Not authorized - no organization found");
}
const evidence = await db.controlRequirement.findMany({
where: {
type: RequirementType.evidence,
},
});
const organizationEvidence = await db.organizationEvidence.createMany({
data: evidence.map((evidence) => ({
organizationId: user.organizationId!,
evidenceId: evidence.id,
name: evidence.name,
description: evidence.description,
})),
});
return organizationEvidence;
};
const createOrganizationEvidence = async (user: User) => {
if (!user.organizationId) {
throw new Error("Not authorized - no organization found");
}
const BATCH_SIZE = 1000;
try {
return await db.$transaction(async (tx) => {
const evidence = await tx.controlRequirement.findMany({
where: {
type: RequirementType.evidence,
},
});
const chunks = [];
for (let i = 0; i < evidence.length; i += BATCH_SIZE) {
chunks.push(evidence.slice(i, i + BATCH_SIZE));
}
const results = await Promise.all(
chunks.map((chunk) =>
tx.organizationEvidence.createMany({
data: chunk.map((evidence) => ({
organizationId: user.organizationId!,
evidenceId: evidence.id,
name: evidence.name,
description: evidence.description,
})),
})
)
);
return results.reduce(
(acc, result) => ({
count: acc.count + result.count,
}),
{ count: 0 }
);
});
} catch (error) {
console.error("Error creating organization evidence:", error);
throw new Error("Failed to create organization evidence");
}
};

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default async function Layout({
const t = await getI18n();

return (
<div className="max-w-[1200px]">
<div className="max-w-[1200px] m-auto">
<SecondaryMenu items={[{ path: "/", label: t("overview.title") }]} />

<main className="mt-8">{children}</main>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { useOrganizationControl } from "../hooks/useOrganizationControl";
import { Card } from "@bubba/ui/card";
import { Label } from "@bubba/ui/label";
import { Button } from "@bubba/ui/button";
import { ArrowLeft, CheckCircle2, XCircle } from "lucide-react";
import { ArrowLeft } from "lucide-react";
import { useRouter } from "next/navigation";
import { useOrganizationControlRequirements } from "../hooks/useOrganizationControlRequirements";
import Link from "next/link";
import { useOrganizationControlProgress } from "../hooks/useOrganizationControlProgress";
import { DataTable } from "./data-table/data-table";

interface SingleControlProps {
controlId: string;
Expand Down Expand Up @@ -66,53 +66,7 @@ export const SingleControl = ({ controlId }: SingleControlProps) => {

<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">Requirements</h1>
<table className="w-full border-collapse">
<thead>
<tr className="border-b">
<th className="text-left p-4 font-medium">Type</th>
<th className="text-left p-4 font-medium">Description</th>
<th className="text-left p-4 font-medium">Status</th>
</tr>
</thead>
<tbody>
{requirements?.map((requirement) => {
const url =
requirement.type === "policy"
? `/policies/${requirement.organizationPolicy?.policy?.id}`
: "_blank";

const isCompleted =
requirement.type === "policy"
? requirement.organizationPolicy?.status === "published"
: false;

return (
<tr
key={requirement.id}
className="border-b hover:bg-muted/50 cursor-pointer"
>
<td className="p-4">
<Link href={url} className="block w-full h-full">
{requirement.type}
</Link>
</td>
<td className="p-4">
<Link href={url} className="block w-full h-full truncate">
{requirement.description}
</Link>
</td>
<td className="p-4 flex items-center justify-center">
{isCompleted ? (
<CheckCircle2 size={16} className="text-green-500" />
) : (
<XCircle size={16} className="text-red-500" />
)}
</td>
</tr>
);
})}
</tbody>
</table>
{requirements && <DataTable data={requirements} />}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import type { ColumnDef } from "@tanstack/react-table";
import { CheckCircle2, XCircle } from "lucide-react";

export interface RequirementType {
id: string;
type: string;
description: string | null;
organizationPolicy?: {
policy?: {
id: string;
name: string;
};
status?: string;
} | null;
}

export const columns: ColumnDef<RequirementType>[] = [
{
id: "type",
accessorKey: "type",
header: "Type",
cell: ({ row }) => row.original.type,
},
{
id: "description",
accessorKey: "description",
header: "Description",
cell: ({ row }) => (
<div className="truncate">{row.original.description}</div>
),
},
{
id: "status",
accessorKey: "organizationPolicy.status",
header: "Status",
cell: ({ row }) => {
const requirement = row.original;
const isCompleted =
requirement.type === "policy"
? requirement.organizationPolicy?.status === "published"
: false;

return (
<div className="flex items-center justify-center">
{isCompleted ? (
<CheckCircle2 size={16} className="text-green-500" />
) : (
<XCircle size={16} className="text-red-500" />
)}
</div>
);
},
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { TableHead, TableHeader, TableRow } from "@bubba/ui/table";

type Props = {
table?: {
getIsAllPageRowsSelected: () => boolean;
getIsSomePageRowsSelected: () => boolean;
getAllLeafColumns: () => {
id: string;
getIsVisible: () => boolean;
}[];
toggleAllPageRowsSelected: (value: boolean) => void;
};
loading?: boolean;
};

export function DataTableHeader({ table, loading }: Props) {
const isVisible = (id: string) =>
loading ||
table
?.getAllLeafColumns()
.find((col) => col.id === id)
?.getIsVisible();

return (
<TableHeader>
<TableRow className="hover:bg-transparent">
{isVisible("type") && (
<TableHead className="h-11 px-4 text-left align-middle font-medium">
Type
</TableHead>
)}
{isVisible("description") && (
<TableHead className="h-11 px-4 text-left align-middle font-medium">
Description
</TableHead>
)}
{isVisible("status") && (
<TableHead className="h-11 px-4 text-left align-middle font-medium">
Status
</TableHead>
)}
</TableRow>
</TableHeader>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use client";

import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";

import { Table, TableBody, TableCell, TableRow } from "@bubba/ui/table";
import { type RequirementType, columns } from "./columns";
import { DataTableHeader } from "./data-table-header";
import { useRouter } from "next/navigation";

interface DataTableProps {
data: RequirementType[];
}

export function DataTable({ data }: DataTableProps) {
const router = useRouter();

const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});

const onRowClick = (requirement: RequirementType) => {
if (
requirement.type === "policy" &&
requirement.organizationPolicy?.policy?.id
) {
router.push(`/policies/${requirement.organizationPolicy.policy.id}`);
}
};

return (
<div className="relative w-full">
<div className="overflow-auto rounded-md border">
<Table>
<DataTableHeader table={table} />
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="hover:bg-muted/50 cursor-pointer"
onClick={() => onRowClick(row.original)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="p-4">
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No requirements found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}
3 changes: 0 additions & 3 deletions apps/app/src/app/[locale]/(app)/(dashboard)/controls/page.tsx

This file was deleted.

Loading