Repository: dimas-fahmi/tusktask
Pull request #36: fix: use dedicated fetch function for server projects query
Author: login: dimas-fahmi
Created: 2025-12-16T02:04:38Z | Merged: 2025-12-16T02:17:17Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Refactor**
  * Optimized project data retrieval to use server-side queries instead of client-side API calls.
  * Enhanced project metadata to include description information.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/app/dashboard/projects/detail/[id]/page.tsx b/app/dashboard/projects/detail/[id]/page.tsx
index 675b091..11da4bc 100644
--- a/app/dashboard/projects/detail/[id]/page.tsx
+++ b/app/dashboard/projects/detail/[id]/page.tsx
@@ -1,8 +1,7 @@
 import type { Metadata } from "next";
-import { headers } from "next/headers";
 import { redirect } from "next/navigation";
 import z from "zod";
-import type { V1ProjectGetResponse } from "@/app/api/v1/project/get";
+import { getProjects } from "@/src/lib/queries/serverQueries/getProjects";
 import ProjectDetailPageIndex from "./ProjectDetailPageIndex";
 
 const uuid = z.uuid();
@@ -13,22 +12,9 @@ export async function generateMetadata({
   params: Promise<{ id: string }>;
 }): Promise<Metadata> {
   const { id } = await params;
-  const validation = uuid.safeParse(id);
 
-  const projectQuery = validation.success
-    ? await fetch(
-        `${process.env.NEXT_PUBLIC_APP_URL}/api/v1/project?id=${id}`,
-        {
-          headers: await headers(),
-          next: {
-            revalidate: 60 * 60 * 12,
-            tags: ["project", id],
-          },
-        },
-      )
-    : undefined;
-  const queryResult = (await projectQuery?.json()) as V1ProjectGetResponse;
-  const projectResult = queryResult?.result?.result?.[0];
+  const query = await getProjects({ id }, ["project", `project-${id}`]);
+  const projectResult = query?.result?.result?.[0];
 
   return {
     title: `${projectResult?.name || "Project Not Found"} | TuskTask`,
diff --git a/src/lib/queries/serverQueries/getProjects.ts b/src/lib/queries/serverQueries/getProjects.ts
new file mode 100644
index 0000000..821e35e
--- /dev/null
+++ b/src/lib/queries/serverQueries/getProjects.ts
@@ -0,0 +1,31 @@
+import type { V1ProjectGetRequest } from "@/app/api/v1/project/get";
+import { objectToQueryString } from "../../utils/objectToQueryString";
+import { parseCookies } from "../../utils/parseCookies";
+
+export async function getProjects(req?: V1ProjectGetRequest, key?: string[]) {
+  const cookieString = await parseCookies();
+  const headers = new Headers();
+  headers.set("cookie", cookieString);
+
+  const queryString = objectToQueryString(req);
+
+  const response = await fetch(
+    `${process.env.NEXT_PUBLIC_APP_URL}/api/v1/project?${queryString}`,
+    {
+      headers: headers,
+      cache: "force-cache",
+      next: {
+        revalidate: 60 * 60 * 2,
+        tags: key || ["project", JSON.stringify(req)],
+      },
+    },
+  );
+
+  const result = await response.json();
+
+  if (!response.ok) {
+    throw result;
+  }
+
+  return result;
+}
diff --git a/src/lib/utils/parseCookies.ts b/src/lib/utils/parseCookies.ts
new file mode 100644
index 0000000..dfcd1a0
--- /dev/null
+++ b/src/lib/utils/parseCookies.ts
@@ -0,0 +1,14 @@
+import { cookies } from "next/headers";
+
+export async function parseCookies(): Promise<string> {
+  // Extract Cookie
+  const cookieStore = await cookies();
+  const allCookies = cookieStore.getAll();
+
+  // Parse cookies
+  const cookieString = allCookies
+    .map((cookie) => `${cookie.name}=${cookie.value}`)
+    .join("; ");
+
+  return cookieString;
+}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/lib/queries/serverQueries/getProjects.ts (1)</summary><blockquote>

`21-27`: **Consider enhancing error handling for better debugging.**

The current error handling simply throws the parsed result. Consider adding context about the request or wrapping the error for better debugging.



Apply this diff for improved error handling:

```diff
   const result = await response.json();

   if (!response.ok) {
-    throw result;
+    throw new Error(`Failed to fetch projects: ${response.status} ${response.statusText}`, {
+      cause: result
+    });
   }

   return result;
```

</blockquote></details>
<details>
<summary>app/dashboard/projects/detail/[id]/page.tsx (2)</summary><blockquote>

`16-17`: **Consider adding type safety for the deeply nested result access.**

The access pattern `query?.result?.result?.[0]` is fragile and suggests the response structure lacks proper typing. Consider defining a typed response interface or adding a type guard helper.



Create a helper function with proper typing:

```typescript
// Add to getProjects.ts or create a types file
interface ProjectQueryResponse {
  result?: {
    result?: Array<{
      name?: string;
      description?: string;
      // ... other project fields
    }>;
  };
}

function getFirstProject(query: ProjectQueryResponse) {
  return query?.result?.result?.[0];
}
```

Then use it in the page:

```diff
-  const query = await getProjects({ id }, ["project", `project-${id}`]);
-  const projectResult = query?.result?.result?.[0];
+  const query = await getProjects({ id }, ["project", `project-${id}`]) as ProjectQueryResponse;
+  const projectResult = getFirstProject(query);
```

---

`9-23`: **Add error handling to gracefully degrade when project fetch fails.**

If `getProjects` throws an error, `generateMetadata` will fail and potentially show an error page. Consider wrapping the call in try-catch to provide fallback metadata.



Apply this diff:

```diff
 export async function generateMetadata({
   params,
 }: {
   params: Promise<{ id: string }>;
 }): Promise<Metadata> {
   const { id } = await params;

-  const query = await getProjects({ id }, ["project", `project-${id}`]);
-  const projectResult = query?.result?.result?.[0];
+  let projectResult;
+  try {
+    const query = await getProjects({ id }, ["project", `project-${id}`]);
+    projectResult = query?.result?.result?.[0];
+  } catch (error) {
+    console.error('Failed to fetch project for metadata:', error);
+    // projectResult remains undefined
+  }

   return {
     title: `${projectResult?.name || "Project Not Found"} | TuskTask`,
     description: projectResult?.description || "No description",
   };
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between cfe37848f4e021af1beba953e7db3bce19908740 and 857cc97574f56aa6f608c772f406880793691363.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `app/dashboard/projects/detail/[id]/page.tsx` (2 hunks)
* `src/lib/queries/serverQueries/getProjects.ts` (1 hunks)
* `src/lib/utils/parseCookies.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code graph analysis (2)</summary>

<details>
<summary>src/lib/queries/serverQueries/getProjects.ts (2)</summary><blockquote>

<details>
<summary>app/api/v1/project/get.ts (1)</summary>

* `V1ProjectGetRequest` (46-46)

</details>
<details>
<summary>src/lib/utils/parseCookies.ts (1)</summary>

* `parseCookies` (3-14)

</details>

</blockquote></details>
<details>
<summary>app/dashboard/projects/detail/[id]/page.tsx (1)</summary><blockquote>

<details>
<summary>src/lib/queries/serverQueries/getProjects.ts (1)</summary>

* `getProjects` (4-28)

</details>

</blockquote></details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-12-16T02:09:01Z

Review comments:
diff_hunk: @@ -0,0 +1,28 @@
+import type { V1ProjectGetRequest } from "@/app/api/v1/project/get";
+import { parseCookies } from "../../utils/parseCookies";
+
+export async function getProjects(req?: V1ProjectGetRequest, key?: string[]) {
+  const cookieString = await parseCookies();
+  const headers = new Headers();
+  headers.set("cookie", cookieString);
+
+  const response = await fetch(
+    `${process.env.NEXT_PUBLIC_APP_URL}/api/v1/project`,
+    {
+      headers: headers,
+      cache: "force-cache",
+      next: {
+        revalidate: 60 * 60 * 2,
+        tags: key || ["project", JSON.stringify(req)],
+      },
+    },
+  );
path: src/lib/queries/serverQueries/getProjects.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_ | _🔴 Critical_

**The `req` parameter is never used in the fetch request.**

The function accepts a `req` parameter (e.g., `{ id }` from the page), but it's never included in the fetch call. The API endpoint at line 10 doesn't receive any query parameters or request body, so it will fetch all projects instead of filtering by the provided request.



You need to either:
1. Add the request parameters to the URL as query params
2. Send the request as a POST body
3. Remove the unused parameter if filtering happens elsewhere

If using query params, apply this diff:

```diff
 export async function getProjects(req?: V1ProjectGetRequest, key?: string[]) {
   const cookieString = await parseCookies();
   const headers = new Headers();
   headers.set("cookie", cookieString);

+  const queryParams = req ? `?${new URLSearchParams(req as Record<string, string>).toString()}` : '';
+
   const response = await fetch(
-    `${process.env.NEXT_PUBLIC_APP_URL}/api/v1/project`,
+    `${process.env.NEXT_PUBLIC_APP_URL}/api/v1/project${queryParams}`,
     {
       headers: headers,
       cache: "force-cache",
       next: {
         revalidate: 60 * 60 * 2,
         tags: key || ["project", JSON.stringify(req)],
       },
     },
   );
```

Note: Adjust the query param construction based on the API's expected format.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/lib/queries/serverQueries/getProjects.ts around lines 4 to 19, the req
parameter is accepted but never sent to the API so filtering is not applied;
update the fetch to include req as query parameters (or remove the parameter if
unused). Specifically, when req is provided, serialize its fields into a query
string (e.g., via URLSearchParams or manual encoding) and append it to
`${process.env.NEXT_PUBLIC_APP_URL}/api/v1/project` (ensure undefined/empty
values are skipped), keep the existing headers and cache/next options, and
continue to use key || ["project", JSON.stringify(req)] for tagging;
alternatively, if the API expects a POST, change the request to POST with JSON
body and appropriate Content-Type header, or delete the unused req parameter if
filtering is handled elsewhere.
```

</details>

<!-- fingerprinting:phantom:poseidon:puma -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 54444f0
created_at: 2025-12-16T02:08:59Z
author_association: NONE
diff_hunk: @@ -0,0 +1,28 @@
+import type { V1ProjectGetRequest } from "@/app/api/v1/project/get";
+import { parseCookies } from "../../utils/parseCookies";
+
+export async function getProjects(req?: V1ProjectGetRequest, key?: string[]) {
+  const cookieString = await parseCookies();
+  const headers = new Headers();
+  headers.set("cookie", cookieString);
+
+  const response = await fetch(
+    `${process.env.NEXT_PUBLIC_APP_URL}/api/v1/project`,
+    {
+      headers: headers,
+      cache: "force-cache",
+      next: {
+        revalidate: 60 * 60 * 2,
+        tags: key || ["project", JSON.stringify(req)],
+      },
path: src/lib/queries/serverQueries/getProjects.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_ | _🟠 Major_

<details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:

```shell
# Read the file to understand what it does
head -50 src/lib/queries/serverQueries/getProjects.ts
```

Repository: dimas-fahmi/tusktask

Length of output: 763

---

🏁 Script executed:

```shell
# Check for Next.js version in package.json
cat package.json | grep -A 2 -B 2 "next"
```

Repository: dimas-fahmi/tusktask

Length of output: 400

---

🏁 Script executed:

```shell
# Search for API routes that might be related to projects
fd -e ts -e js "route\.(ts|js)" | head -20
```

Repository: dimas-fahmi/tusktask

Length of output: 277

---

🏁 Script executed:

```shell
# Check the API route handler configuration
cat -n app/api/v1/project/route.ts | head -50
```

Repository: dimas-fahmi/tusktask

Length of output: 243

---

🏁 Script executed:

```shell
# Search for dynamic or caching exports in the API route
rg "export.*dynamic|export.*const.*revalidate|export.*const.*cache" app/api/v1/project/route.ts
```

Repository: dimas-fahmi/tusktask

Length of output: 46

---

🏁 Script executed:

```shell
# Check the GET handler implementation
cat -n app/api/v1/project/get.ts
```

Repository: dimas-fahmi/tusktask

Length of output: 5993

---

🏁 Script executed:

```shell
# Check if there are any dynamic exports or caching config in the project directory
fd "\.ts$" app/api/v1/project/ | xargs rg "export.*dynamic|export.*revalidate"
```

Repository: dimas-fahmi/tusktask

Length of output: 48

---

🌐 Web query:

`Next.js 16 fetch caching behavior default GET requests`

💡 Result:

Short answer: In Next.js 16, GET fetches in Server Components are not cached by default — the default behavior is equivalent to cache: 'no-store' (you must opt into caching with cache: 'force-cache', next.revalidate, or next.tags). Note: older Next.js docs (v13/14) described fetch as cached by default; the behavior changed in later versions. ([nextjs.org](https://nextjs.org/docs/app/getting-started/caching-and-revalidating?utm_source=openai))

Citations:

- 1: https://nextjs.org/docs/app/getting-started/caching-and-revalidating?utm_source=openai

---

🌐 Web query:

`Next.js 16 route handler caching ISR fetch cache force-cache`

💡 Result:

Short answer: Use fetch(..., { cache: 'force-cache' }) (or fetch(..., { next: { revalidate: <seconds> } }) to get ISR behavior) inside route handlers/pages—Next.js will store the response in the Data Cache and serve it while fresh, then revalidate when the TTL expires. Key points:

- Enable caching: fetch(url, { cache: 'force-cache' }) caches the request in the Data Cache. Use next.revalidate: number to set an explicit ISR TTL (seconds). [1][2]  
- Revalidation: fetch(..., { next: { revalidate: 3600 } }) revalidates that cached entry after 3600s; revalidate on-demand via tags (next.tags) and revalidateTag/revalidatePath where appropriate. [1][2]  
- Defaults & dynamic routes: Next.js defaults to force-cache for static routes (but will behave as no-store if dynamic APIs/headers/cookies/dynamic route flags are present). If you omit cache and the route is dynamic, it will act as no-store. [1][3]  
- Conflicts/errors: Conflicting options (e.g., { revalidate: 0, cache: 'force-cache' }) are invalid and cause errors. Also using Authorization/Cookie headers or calling headers()/cookies() can opt-out of caching. [1][3]  
- Full-route vs data cache: The Data Cache persists across requests/deployments; fetch cache + route full-page cache combine to produce ISR for route handlers. Use revalidateTag/revalidatePath for targeted invalidation. [2][4]

If you want, tell me your exact route-handler code and desired TTL/behavior and I’ll give the exact fetch snippet and where to call revalidate. 

Sources: Next.js docs — fetch API; Caching & Revalidating; Caching guide. [1][2][3][4]

---



</details>

**Remove or reconfigure caching strategy—the target API route handler is dynamic and cannot be cached.**

The API route handler at `/api/v1/project` calls `headers()` and validates sessions (user-specific), making it inherently dynamic. In Next.js 16, route handlers that use dynamic APIs like `headers()` cannot leverage ISR caching. The `cache: "force-cache"` and `revalidate` options on this fetch call will not provide the expected caching behavior since the handler itself cannot be cached. Either make the route handler static (if possible) or remove the caching configuration from the fetch call.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/lib/queries/serverQueries/getProjects.ts around lines 13 to 17, the fetch
call sets cache: "force-cache" and a revalidate tag even though the target API
route is dynamic (uses headers() and session validation) and cannot be
ISR-cached; remove the cache and next.revalidate/next.tags options from the
fetch call (or set it to cache: "no-store") so the request is always dynamic, or
alternatively make the API route truly static if you intend to use ISR—update
the fetch options to omit caching configuration accordingly.
```

</details>

<!-- fingerprinting:phantom:poseidon:puma -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-12-16T02:08:59Z
author_association: NONE
diff_hunk: @@ -0,0 +1,14 @@
+import { cookies } from "next/headers";
+
+export async function parseCookies(): Promise<string> {
+  // Extract Cookie
+  const cookieStore = await cookies();
+  const allCookies = cookieStore.getAll();
+
+  // Parse cookies
+  const cookieString = allCookies
+    .map((cookie) => `${cookie.name}=${cookie.value}`)
+    .join(";");
path: src/lib/utils/parseCookies.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_ | _🟡 Minor_

**Fix cookie delimiter to comply with RFC 6265.**

The Cookie header should separate cookie pairs with `"; "` (semicolon + space), not just `";"`. This ensures compliance with RFC 6265 cookie formatting standards.



Apply this diff:

```diff
   const cookieString = allCookies
     .map((cookie) => `${cookie.name}=${cookie.value}`)
-    .join(";");
+    .join("; ");
```

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion
  const cookieString = allCookies
    .map((cookie) => `${cookie.name}=${cookie.value}`)
    .join("; ");
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/lib/utils/parseCookies.ts around lines 9 to 11, the Cookie header is
built using ";" as the delimiter which does not follow RFC 6265; change the join
to use "; " (semicolon followed by a space) so cookie pairs are separated with
"; " to comply with the spec and ensure proper formatting.
```

</details>

<!-- fingerprinting:phantom:poseidon:puma -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-12-16T02:08:59Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-12-16T02:04:43Z
body: [vc]: #dMev9Ngu/g4SsLKLcE7k7ql0hilFMhCykEVjMEN0QNA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0dXNrdGFzayIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9kaW1hcy1mYWhtaXMtcHJvamVjdHMvdHVza3Rhc2svREpGNjlYOHlvdVYzd3JuWEZRdlRNeWppZDdCWiIsInByZXZpZXdVcmwiOiJ0dXNrdGFzay1naXQtcGFnZS1wcm9qZWN0cy1kaW1hcy1mYWhtaXMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ0dXNrdGFzay1naXQtcGFnZS1wcm9qZWN0cy1kaW1hcy1mYWhtaXMtcHJvamVjdHMudmVyY2VsLmFwcCJ9LCJyb290RGlyZWN0b3J5IjpudWxsfV19
The latest updates on your projects. Learn more about [Vercel for GitHub](https://vercel.link/github-learn-more).

| Project | Deployment | Review | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| [tusktask](https://vercel.com/dimas-fahmis-projects/tusktask) | ![Ready](https://vercel.com/static/status/ready.svg) [Ready](https://vercel.com/dimas-fahmis-projects/tusktask/DJF69X8youV3wrnXFQvTMyjid7BZ) | [Preview](https://tusktask-git-page-projects-dimas-fahmis-projects.vercel.app), [Comment](https://vercel.live/open-feedback/tusktask-git-page-projects-dimas-fahmis-projects.vercel.app?via=pr-comment-feedback-link) | Dec 16, 2025 2:15am |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-12-16T02:04:49Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: failure by coderabbit.ai -->

> [!CAUTION]
> ## Review failed
> 
> The head commit changed during the review from 54444f0bbbe4278ef2a3858a0eff52567cf186b8 to 5f43c02e738aaf844f6ffdaa41d734502ff9019e.

<!-- end of auto-generated comment: failure by coderabbit.ai -->

<!-- walkthrough_start -->

## Walkthrough

Migrates project metadata generation from client-side API fetch to server-side query pattern. Introduces `getProjects` server utility and `parseCookies` helper to handle server-side project data retrieval with authentication cookies.

## Changes

| Cohort / File(s) | Change Summary |
|---|---|
| **Page Component Updates** <br> `app/dashboard/projects/detail/[id]/page.tsx` | Replaced client-side API fetch in `generateMetadata` with server-side `getProjects` query; removed `next/headers` and response type imports; updated project description derivation |
| **Server Query Utilities** <br> `src/lib/queries/serverQueries/getProjects.ts` | New server-side function to fetch project data from API with cookie authentication, force-cache strategy, and 2-hour revalidation tags |
| **Cookie Utilities** <br> `src/lib/utils/parseCookies.ts` | New utility function to parse cookies from Next.js headers into formatted semicolon-separated string |

## Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

- Cookie parsing logic correctness and Promise handling
- Cache revalidation tags configuration and appropriateness for project data
- Error handling in `getProjects` when API responses fail
- Impact of metadata generation pattern change on static generation and SEO

## Possibly related PRs

- **#18**: Implements the `app/api/v1/project` GET endpoint handler and types that the new `getProjects` server query depends on.
- **#36**: Implements alternative DB-backed server fetch for the same `app/dashboard/projects/[id]/page.tsx` project detail page.

## Poem

> 🐰 Cookies crumble through the headers, neat,
> Server-side queries make metadata complete,
> No more client-side fetch from the distant shore,
> Data flows server-fast, we ask for no more! ✨

<!-- walkthrough_end -->

<!-- pre_merge_checks_walkthrough_start -->

## Pre-merge checks and finishing touches
<details>
<summary>❌ Failed checks (1 warning)</summary>

|     Check name     | Status     | Explanation                                                                          | Resolution                                                                     |
| :----------------: | :--------- | :----------------------------------------------------------------------------------- | :----------------------------------------------------------------------------- |
| Docstring Coverage | ⚠️ Warning | Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. | You can run `@coderabbitai generate docstrings` to improve docstring coverage. |

</details>
<details>
<summary>✅ Passed checks (2 passed)</summary>

|     Check name    | Status   | Explanation                                                                                                                                                              |
| :---------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                              |
|    Title check    | ✅ Passed | The title accurately describes the main change: introducing a dedicated server-side fetch function (getProjects) to replace client-side API calls in the projects query. |

</details>

<!-- pre_merge_checks_walkthrough_end -->

<!-- tips_start -->

---

Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=dimas-fahmi/tusktask&utm_content=36)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>

<!-- tips_end -->

<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEejqANiS4AzeAA8u2RCUhLa8BtTqQbJXAywvtgYYvD4WDb4fC4UUny8+CJiyACO2JSyBgBy2MwClFwAzABsBgCqAEoAMlywuLjciBwA9C1E6rDYAhpMzC0ezGiIYDZosMzwLbjOANa4w7Mt3NgWFi2lFbFcg8Oj45MGAMr42BQMrgJUobBc3GikyxRJoriIBtBoFKS4kFeYgS4Q3gGGOCxmzX43DIBkqJAk8BIAHdKJCDNUVCQLGiAMIUEjeejULgAJgADCSAKxgACMJNpJWgFI4ZIALBwigAOABaRn0xnAUDI9HwNhwBGIZGUNHofTYGE4PD4gmS4ikMnkTCUVFU6i0On5JigcFQqEw4sIpHIVBlClY7C4VCRkEQeSGFHkcgU2pUak02l0YEMAtMBjQ3G4A2GsAE+C+tCeLxSA382nWAG14LQALrLB4kDRvRwGABEZYMAGIK5AAIIASUl1sJLrdX3kosYsEwpHeBigcO4FjQF1lFkRCrAiCzrhrAAU675/IFIAAKJGdSBkBHPDDy3BgATDHxVaroDD0WAE7WIACUKCwVulJAAsqnaNQ0JB17ggp/YvFJ2nSB0kySAEU/H5Z2eVVEBXABvFB6AAXwAGkgdMS0SVUSzQgADLDXjAAASOCsyQ3DsxvDQYEvfgBH/FQrEgfFXQsX4MHwZ0+mkXxnmYSAf1cECPUATAJkHIRBbRY1ZfjsLF6BBSSr34MV7goKcMCIdBFwCS96AAKSOAB5bJmOkbgIhcDQ+0gOFmHwKQFOYCyKF+DtyHsXAWkvNBrzPegADUaSgpNcAAcX8OFEAsjAXAE2RoUQABudBaHcFBnOiWTokgSDoNeZAbD4gTaP/ShgIyD1IDweAx1wWRrKgELVTcaQGAoeBuHECIUHEzjWo6xzeJYErXHxXAOvhHwCLEKKZNXEJAm7HxJIobAxDOEg0IEPBIDYBZ3wWSAOOdEEGAsbAlGQK72s67qsBBVKPHugT8AEywSCoowjCrWs2OlcJYte0bWvOr5qEB5AOxIewXNtHKVgEMcGE3BV1ERXsoGyN6ls0niCBdeAiAwahNqhsUYbhuhlm6ZHUfEcRpGsytq2fTB4D8STIAAMVqmcSYsWQAC9KCMABRST4CGW0tTG+FEWdEgbCiVyuFfDw8lLcs+1DRBzhaMcBBaYSMZaMqKAARUq028tCxBC2aLWSxZ2sG0fG0VtbKqO1xnsjDrBVnlodaeM/chnXNwClGq8Q6vkGxFpe22YJK6gdMCHiZt+Q7PyKkbBNrec0OoBZAhBLSmHwWYMeG/i1JcHF8CrmuCYL/EQMk6i61+JhYom9a3m0yvq9cHztTQ6EKBV5hkFzpcgm/IIVYuMAvAz/zIGyGHNCEZB8QkNAx0OwGoXu5AV33w+s28F1/GBklIFgU51JShYiGuyhJB8PO6+eBF0tmCQdsfAC6xHgFfEW9B24ZEkjeCeXwXDIALoZEyZloqWS2hvH8zwkSzywJQZ4fAkSXiwCxGKVkq69V8IfKytl/BnCBgXeuPhpJsX4FgV0DALiIHtpAMWsMspINognUIL1EbI0FilKWcNkBnQutHIKzVXgRVwHCDuvx6rQg3swxuzceI1Tjszb6UBXw/kUE9OgXBcLJwKhfEgqQAD8XBFH5TECotRMDcBoUAbIJxLoJrl3TJRXC95IC4T1gwA28AjYm2kGbSg8QrafziTYlIDtcLi0ltLHwsszIImRJuZWWU1Z0HgJrMszsdZgCMBEqJRsDGIDzOpEguiR72zeBwJ2Lt6yNifPQV0rA2wqU7MtTGtY0qz2OgUgx6h5Bp1qYbFoDSmkNybm0h2qdfiU0EWedAiBZA3B3KcQqicT46LWRjFcN4uAhUmC4YAq1y56C7r8cak11QKD0cgcCUzPLeSvKiNCggFiKXQGsT5bS0K93iIPQS/EQQExLCTNgABeA+F0SAlh4NodSxdzyQCEPgUFcKvwbhcJMJgFhLLA3Ggw7SGkiBMUeZpIxNlTFPyJGlSxYTzlfKuTcviSASAPICZpPQITHp2CYuE/Wizlm8vWW8DJLs2YYA5tIX4vMmI1gFsLUWBgJbiGybKRQct8mKyKarSA6synMC6TrYMRpUYijFGgPAlopQexNfaBUjo0ARy9p6TUpqdR+n1IGQwTq5TqAAPpZkQDG/eCs6Axskl8X4/IDBOs5JSAA7FwgAnLmvNrIbCUhKGgNAJQbAlDJJyBgubc0khsKyMkJROScjJLmgtpQC00lKEUCNIYoDRtwHG2gCak3IhTcKIdTreAkBjWwb4i6M4MFmAmtNrkh0GDggYSAkASxIFnJUAAQlS9ddBG4+twLOfASlaAllsDQra+7D2ICfqsWg578DrpPU+6h2JX0HqPYgIy8QOpcowABsYQGUJvpLB4WglQQgABFf1HFFe/HEl510Af7sBw9SGUMYHMLgKwOHRCzHw2tQjiGswkdQ21DqXVAaUbw8+uDCGxwYEAbQOsPCYGYYoABss8GQNDkkux2Yc02KIAA+mN9B690HtU4etdsxshoDYKJpjiBbqsZ6tJnCSnVMljTRCGjGRxNqcPZTIcJN7qieky6auEYfBQEbkoSovp1BiUfkTWAYArBSAsC2QZVVUBkEYnQDQJnbOHvskoUTSIvhqs0vF2zJZohExBIfaTWmdNcEQ8xu6gNnZqdQqZlTWWNOFcxcVsjTENOZbU+Z8EzgrOEba/ZzAEMIiibgK4BmTFhwMDON4QWrV9MdQKEI1wwIsC+2sPeCaih1rl20u4TwzZI5Tmjn4XSwRREnxXKkt4d4Cb4kHMOVw51xz7n2zOecjBD7YlCUw1xg9hINVa2ZpLDXD2pYoOlogf2QP4l7nYIgm0YMvps21nLHQBYFe04DksI3MWmaq2pmrbW6to907+5lWlG7xHzOD99HX5OcZcAjszvXHPleK+hhgJOFDk9IFQskGgyRkgAKRfgQMuU0sVsDK08A96iQ2zLpHgPieg2DpBPwsApZAnbecC7i/TiH0h8AXSc8VgAmqcV7pCQhhIAAKy1DeoAM7sb60GJ1hxAISCbSL/q4J3bOsMc+UKQbXpmQMA5S2l8ulPssdWR/l3DmnCfFe9yT+T2O33ZhsyWSTt7KhNfRzSGttaihkkpP2ooXJaCciKAWitDAyRcgEEUNeJJc1F5sDSTkAgK1kgLWvFQ/bc00jQCQEkAgGA2ALXmzk4/q+ZcQ87jqmkyf+5IAHGgIPD6Ye8ABvH77ZXRKWbHRpCqMYOy30Hw9BAFgWG5qcyyAGaQ68PSIsIlkADqnRWdJ4A2SbH9PzO75idbHEubEkpNI0udu0jTpANviBhfofNfidrflwPfmfiWE/mfG/j+B/i7l/j/qZiWOGJGO+B+rGPGImDBCmCChmFmLmPcAHkWKfglhjvgJfvAc/rFHfg/qgTfrFBgbAFgfPu/DgZVm+khAYKIVmoKEqIusuqQDGhpgmrOvoEAA -->

<!-- internal state end -->
author_association: NONE