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
96 changes: 62 additions & 34 deletions apps/app/src/actions/framework/select-frameworks-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@ export const selectFrameworksAction = authActionClient
}

try {
await Promise.all([
// First create categories
await createOrganizationCategories(user as User, frameworkIds);

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

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

Comment on lines 32 to 45
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

Wrap operations in a transaction for atomicity.

The sequential operations (creating categories, frameworks, and policies) should be wrapped in a transaction to ensure atomicity. If any operation fails, all changes should be rolled back.

    try {
+     return await db.$transaction(async (tx) => {
        // First create categories
-       await createOrganizationCategories(user as User, frameworkIds);
+       await createOrganizationCategories(user as User, frameworkIds, tx);

        // Then create frameworks and controls
        await Promise.all(
          frameworkIds.map((frameworkId) =>
-           createOrganizationFramework(user as User, frameworkId)
+           createOrganizationFramework(user as User, frameworkId, tx)
          )
        );

        // Finally create policies
-       await createOrganizationPolicy(user as User, frameworkIds);
+       await createOrganizationPolicy(user as User, frameworkIds, tx);

-       return {
-         data: true,
-       };
+       return { data: true };
+     });
    } catch (error) {
📝 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
try {
await Promise.all([
// First create categories
await createOrganizationCategories(user as User, frameworkIds);
// Then create frameworks and controls
await Promise.all(
frameworkIds.map((frameworkId) =>
createOrganizationFramework(user as User, frameworkId)
),
createOrganizationPolicy(user as User, frameworkIds),
]);
)
);
// Finally create policies
await createOrganizationPolicy(user as User, frameworkIds);
try {
return await db.$transaction(async (tx) => {
// First create categories
await createOrganizationCategories(user as User, frameworkIds, tx);
// Then create frameworks and controls
await Promise.all(
frameworkIds.map((frameworkId) =>
createOrganizationFramework(user as User, frameworkId, tx)
)
);
// Finally create policies
await createOrganizationPolicy(user as User, frameworkIds, tx);
return { data: true };
});
} catch (error) {

return {
data: true,
Expand Down Expand Up @@ -65,46 +71,40 @@ const createOrganizationFramework = async (user: User, frameworkId: string) => {
},
});

// For each framework we need to get the categories and controls.
const framework = await db.framework.findUnique({
where: { id: frameworkId },
});

if (!framework) {
throw new Error("Framework not found");
}

// Get the framework categories and their corresponding organization categories
const frameworkCategories = await db.frameworkCategory.findMany({
where: { frameworkId },
select: {
id: true,
include: {
controls: true,
},
});

// For each category we need to get the controls.
const frameworkControls = await db.control.findMany({
// Get the organization categories that were just created
const organizationCategories = await db.organizationCategory.findMany({
where: {
frameworkCategoryId: {
in: frameworkCategories.map((category) => category.id),
},
},
select: {
id: true,
organizationId: user.organizationId,
frameworkId,
},
});

if (!user.organizationId) {
throw new Error("Organization ID is required");
// Create controls for each category
for (const frameworkCategory of frameworkCategories) {
const organizationCategory = organizationCategories.find(
(oc) => oc.name === frameworkCategory.name
);

if (!organizationCategory) continue;

await db.organizationControl.createMany({
data: frameworkCategory.controls.map((control) => ({
organizationFrameworkId: organizationFramework.id,
controlId: control.id,
organizationId: user.organizationId!,
status: "not_started",
organizationCategoryId: organizationCategory.id,
})),
});
}

await db.organizationControl.createMany({
data: frameworkControls.map((control) => ({
organizationFrameworkId: organizationFramework.id,
controlId: control.id,
organizationId: user.organizationId!,
status: "not_started",
})),
});
};

const createOrganizationPolicy = async (user: User, frameworkIds: string[]) => {
Expand Down Expand Up @@ -143,3 +143,31 @@ const createOrganizationPolicy = async (user: User, frameworkIds: string[]) => {

return organizationPolicies;
};

const createOrganizationCategories = async (
user: User,
frameworkIds: string[]
) => {
if (!user.organizationId) {
throw new Error("Not authorized - no organization found");
}

// For each frameworkCategory we need to get the controls.
const frameworkCategories = await db.frameworkCategory.findMany({
where: {
frameworkId: { in: frameworkIds },
},
});

// Create the organization categories.
const organizationCategories = await db.organizationCategory.createMany({
data: frameworkCategories.map((category) => ({
name: category.name,
description: category.description,
organizationId: user.organizationId!,
frameworkId: category.frameworkId,
})),
});

return organizationCategories;
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const FrameworksOverview = () => {
frameworks,
availableFrameworks,
isLoading,
isMutating,
error,
selectFrameworks,
} = useFrameworks();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use server";

import { db, OrganizationControl, Control } from "@bubba/db";
import { authActionClient } from "@/actions/safe-action";
import { z } from "zod";

export interface OrganizationControlResponse {
organizationControl: OrganizationControl & {
control: Control;
};
}

export const getOrganizationControl = authActionClient
.schema(z.object({ controlId: z.string() }))
.metadata({
name: "getOrganizationControl",
track: {
event: "get-organization-control",
channel: "server",
},
})
.action(async ({ ctx, parsedInput }) => {
const { user } = ctx;
const { controlId } = parsedInput;

if (!user.organizationId) {
return {
error: "Not authorized - no organization found",
};
}

try {
const organizationControl = await db.organizationControl.findUnique({
where: {
organizationId: user.organizationId,
id: controlId,
},
include: {
control: true,
},
});

if (!organizationControl) {
return {
error: "Organization control not found",
};
}

return {
data: {
organizationControl,
},
};
} catch (error) {
console.error("Error fetching organization control:", error);
return {
error: "Failed to fetch organization control",
};
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import {
DisplayFrameworkStatus,
StatusType,
} from "@/components/frameworks/framework-status";
import { useOrganizationControl } from "../hooks/useOrganizationControl";
import { Card } from "@bubba/ui/card";
import { Label } from "@bubba/ui/label";

interface SingleControlProps {
controlId: string;
}

export const SingleControl = ({ controlId }: SingleControlProps) => {
const { data: control } = useOrganizationControl(controlId);
if (!control) return null;
Comment on lines +16 to +17
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

Consider handling loading and error states.

The component currently returns null when data is not available, but it should handle loading and error states explicitly.

-  const { data: control } = useOrganizationControl(controlId);
-  if (!control) return null;
+  const { data: control, isLoading, error } = useOrganizationControl(controlId);
+
+  if (isLoading) return <div>Loading...</div>;
+  if (error) return <div>Error: {error.message}</div>;
+  if (!control) return <div>No control found</div>;
📝 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 { data: control } = useOrganizationControl(controlId);
if (!control) return null;
const { data: control, isLoading, error } = useOrganizationControl(controlId);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!control) return <div>No control found</div>;


return (
<div className="max-w-[1200px] mx-auto py-8 gap-4 flex flex-col">
<div className="flex flex-row justify-between items-center">
<h1 className="text-3xl font-bold">{control?.control.name}</h1>
<DisplayFrameworkStatus
status={control?.status.toLowerCase() as StatusType}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Card className="flex flex-col gap-2 p-4 px-8 min-h-[200px]">
<div>
<Label className="text-lg">Description</Label>
<p className="text-sm">{control?.control.description}</p>
</div>
</Card>
<Card className="gap-2 p-4 px-8 grid grid-cols-2">
<div>
<Label className="text-lg">Code</Label>
<h1 className="text-sm">{control?.control.code}</h1>
</div>
<div>
<Label className="text-lg">Domain</Label>
<p className="text-sm">{control?.control.domain}</p>
</div>
</Card>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import { OrganizationControl } from "@bubba/db";
import useSWR from "swr";
import {
getOrganizationControl,
OrganizationControlResponse,
} from "../Actions/getOrganizationControl";

async function fetchOrganizationControl(
controlId: string
): Promise<OrganizationControlResponse> {
const result = await getOrganizationControl({ controlId });

if (!result) {
throw new Error("Failed to fetch control");
}

const data = result.data?.data;
if (!data) {
throw new Error("Invalid response from server");
}

return data;
}

export function useOrganizationControl(controlId: string) {
const { data, error, isLoading, mutate } =
useSWR<OrganizationControlResponse>(
["organization-control", controlId],
() => fetchOrganizationControl(controlId),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);

return {
data: data?.organizationControl,
isLoading,
error,
mutate,
};
}
11 changes: 11 additions & 0 deletions apps/app/src/app/[locale]/(app)/(dashboard)/controls/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SingleControl } from "./Components/SingleControl";

interface PageProps {
params: Promise<{ id: string }>;
}
Comment on lines +3 to +5
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the type of params prop.

The params prop is incorrectly typed as a Promise. In Next.js, page props are already resolved and should not be Promises.

Apply this diff to fix the type:

-  params: Promise<{ id: string }>;
+  params: { id: string };
📝 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
interface PageProps {
params: Promise<{ id: string }>;
}
interface PageProps {
params: { id: string };
}


export default async function SingleControlPage({ params }: PageProps) {
const { id } = await params;

return <SingleControl controlId={id} />;
}
3 changes: 3 additions & 0 deletions apps/app/src/app/[locale]/(app)/(dashboard)/controls/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function ControlsPage() {
return <div>Controls</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use server";

import { db } from "@bubba/db";
import { authActionClient } from "@/actions/safe-action";
import type {
Framework,
OrganizationControl,
OrganizationFramework,
OrganizationCategory,
Control,
} from "@bubba/db";
import { z } from "zod";

export type OrganizationCategoryWithControls = OrganizationCategory & {
organizationControl: (OrganizationControl & {
control: Control;
})[];
};

export interface OrganizationCategoriesResponse {
organizationCategories: OrganizationCategoryWithControls[];
}

export const getOrganizationCategories = authActionClient
.schema(z.object({ frameworkId: z.string() }))
.metadata({
name: "getOrganizationCategories",
track: {
event: "get-organization-categories",
channel: "server",
},
})
.action(async ({ ctx, parsedInput }) => {
const { user } = ctx;
const { frameworkId } = parsedInput;

if (!user.organizationId) {
return {
error: "Not authorized - no organization found",
};
}

try {
const organizationCategories = await db.organizationCategory.findMany({
where: {
organizationId: user.organizationId,
frameworkId,
},
include: {
organizationControl: {
include: {
control: true,
},
},
},
});

if (!organizationCategories) {
return {
error: "Organization categories not found",
};
}
Comment on lines +58 to +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect empty check for findMany result.

The findMany operation returns an empty array when no results are found, not null. The current check is incorrect.

-      if (!organizationCategories) {
+      if (organizationCategories.length === 0) {
         return {
           error: "Organization categories not found",
         };
       }
📝 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
if (!organizationCategories) {
return {
error: "Organization categories not found",
};
}
if (organizationCategories.length === 0) {
return {
error: "Organization categories not found",
};
}


return {
data: organizationCategories,
};
} catch (error) {
console.error("Error fetching organization categories:", error);
return {
error: "Failed to fetch organization categories",
};
}
});
Loading