Skip to content

Refactor Outlook folder functionality#678

Merged
elie222 merged 7 commits intoelie222:mainfrom
edulelis:refactor-move-to-folder-action
Aug 13, 2025
Merged

Refactor Outlook folder functionality#678
elie222 merged 7 commits intoelie222:mainfrom
edulelis:refactor-move-to-folder-action

Conversation

@edulelis
Copy link
Collaborator

@edulelis edulelis commented Aug 13, 2025

Screenshot 2025-08-13 at 12 05 44 Screenshot 2025-08-13 at 12 04 46

Summary by CodeRabbit

  • New Features

    • Hierarchical, searchable folder picker and combobox for selecting Outlook folders (with loading/error states) and per-action folder selection UI.
  • Improvements

    • Actions use stable folder IDs for move/archive operations; folder fields disable variable insertion and pro-tips.
    • Provider-aware rule handling: folder fields are included only for Microsoft accounts; provider threaded through rule tools.
  • Bug Fixes / Validation

    • MOVE_FOLDER validation accepts a selected folder ID or name and shows clearer prompts.
  • Chores

    • Backend and schema updated to store folder IDs; folder list API and client hook added.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 13, 2025

Walkthrough

Adds end-to-end folderId support: DB schema + migration, folder-fetch API and Outlook folder utilities, frontend folder selector components and hook, folderId threaded through rule/action validation, serialization, persistence, and Microsoft provider move/archive logic.

Changes

Cohort / File(s) Summary
Rule form & action UI
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx, apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx, apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
Threads folderId into action defaults and optimistic updates; passes folders and foldersLoading through RuleForm → ActionCard; per-action UI wired to set/clear action.folderName and action.folderId and render folder picker for MOVE_FOLDER.
Folder picker components & hook
apps/web/components/FolderSelector.tsx, apps/web/components/FolderCombobox.tsx, apps/web/hooks/useFolders.ts
Adds FolderSelector and FolderCombobox UI components plus useFolders SWR hook to fetch, search, select, and clear nested Outlook folders; supports loading, search, selection, and error states.
Folders API route & server fetch
apps/web/app/api/user/folders/route.ts, apps/web/utils/outlook/folders.ts
New GET route /api/user/folders (wrapped withEmailAccount) and getOutlookFolders utility that builds a nested OutlookFolder tree; exports OutlookFolder type and FOLDER_SEPARATOR.
Outlook label/message & email provider changes
apps/web/utils/outlook/label.ts, apps/web/utils/outlook/message.ts, apps/web/utils/email/microsoft.ts
Archive/move logic switched to use folderId (existence checks for custom ids); removed path-based folder creation (getOrCreateFolderByName), changed moveThreadToFolder signature to accept folderId, archive uses fixed folderId: "archive".
Prisma schema & migration
apps/web/prisma/schema.prisma, apps/web/prisma/migrations/..._add_folder_id/migration.sql
Adds optional folderId String? to Action, ExecutedAction, ScheduledAction; migration SQL adds folderId TEXT to those tables.
Action shape, validation & sanitization
apps/web/utils/action-item.ts, apps/web/utils/actions/rule.validation.ts, apps/web/app/api/user/rules/[id]/route.ts, apps/web/utils/actions/rule.ts
Adds folderId to action inputs, getActionFields, sanitization, API serialization, and persistence mapping; zod action schema adds folderId and MOVE_FOLDER validation accepts either folderName or folderId.
AI actions & types
apps/web/utils/ai/actions.ts, apps/web/utils/ai/types.ts, apps/web/utils/ai/assistant/chat.ts, apps/web/utils/ai/assistant/process-user-request.ts
move_folder now uses folderId; ActionItem type extended with folderName? and folderId?; threads provider through assistant tools and includes provider when creating/updating rules to conditionally include folderName only for Microsoft.
Scheduled actions persistence
apps/web/utils/scheduled-actions/scheduler.ts
Persists folderName and folderId when creating scheduledAction records.
Rule/action lifecycle & helpers
apps/web/utils/actions/rule.ts, apps/web/utils/actions/rule.validation.ts, apps/web/utils/rule/rule.ts
mapActionFields, create/update rule flows and validation updated to accept and propagate provider and handle folderName only for Microsoft; deleteRuleAction refactor includes revalidation and prompt update hook.
Misc (removed/changed helpers)
apps/web/utils/outlook/message.ts, apps/web/utils/outlook/label.ts
Removed getOrCreateFolderByName and migrated folder-path logic to folderId-based flows with explicit existence checks.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant RuleForm
  participant useFolders
  participant API as "/api/user/folders"
  participant Server as folders route
  participant Outlook as getOutlookFolders
  participant Graph as Microsoft Graph

  User->>RuleForm: open MOVE_FOLDER action editor
  RuleForm->>useFolders: load folders
  useFolders->>API: GET /api/user/folders
  API->>Server: withEmailAccount -> getFolders
  Server->>Outlook: getOutlookFolders()
  Outlook->>Graph: fetch /me/mailFolders (nested)
  Graph-->>Outlook: folders tree
  Outlook-->>Server: OutlookFolder[]
  Server-->>API: JSON folders
  API-->>useFolders: folders
  useFolders-->>RuleForm: folders, isLoading=false
  User->>RuleForm: select folder
  RuleForm-->>RuleForm: set action.folderId + action.folderName
Loading
sequenceDiagram
  participant Runner as Action runner
  participant OutlookProv as OutlookProvider
  participant LabelUtil as archiveThread
  participant Graph as Microsoft Graph

  Runner->>OutlookProv: moveThreadToFolder(threadId, ownerEmail, folderId)
  OutlookProv->>LabelUtil: archiveThread({threadId, folderId})
  LabelUtil->>Graph: move messages destinationId=folderId
  Graph-->>LabelUtil: Success/Failure
  LabelUtil-->>OutlookProv: result
  OutlookProv-->>Runner: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • elie222
  • johnlowe399-blip

Poem

I hop through folders, id in paw,
I fetch the trees and map each straw.
I show the list, you pick the one,
I move the thread and tidy's done.
Rabbit cheers — inbox neat and fun. 🐇📁

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel
Copy link

vercel bot commented Aug 13, 2025

@edulelis is attempting to deploy a commit to the Inbox Zero OSS Program Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🔭 Outside diff range comments (3)
apps/web/utils/scheduled-actions/scheduler.ts (1)

140-155: Remove non-null assertion and fix inferred any[] type for scheduledActions

  • actionItem.delayInMinutes! violates the “don’t use non-null assertions” guideline.
  • scheduledActions is inferred as any[].

Apply this diff in the loop to avoid the non-null assertion and keep types tight:

-  const scheduledActions = [];
+  const scheduledActions: ScheduledAction[] = [];

   for (const actionItem of delayedActions) {
-    const scheduledFor = addMinutes(new Date(), actionItem.delayInMinutes!);
+    if (actionItem.delayInMinutes == null) {
+      // Should be unreachable due to the filter above, but narrows the type safely
+      continue;
+    }
+    const scheduledFor = addMinutes(new Date(), actionItem.delayInMinutes);

And add the missing type import at the top of the file:

// near the other imports
import { ScheduledActionStatus, type ScheduledAction } from "@prisma/client";
apps/web/utils/ai/actions.ts (1)

270-277: Guard against missing folderId and tighten the action args type

The provider call will receive undefined if validation upstream ever regresses. Add a lightweight guard and avoid any.

-const move_folder: ActionFunction<any> = async ({
+const move_folder: ActionFunction<{ folderId?: string | null }> = async ({
   client,
   email,
   userEmail,
   args,
 }) => {
-  await client.moveThreadToFolder(email.threadId, userEmail, args.folderId);
+  if (!args.folderId) {
+    logger.warn("MOVE_FOLDER skipped: missing folderId", {
+      threadId: email.threadId,
+    });
+    return;
+  }
+  await client.moveThreadToFolder(email.threadId, userEmail, args.folderId);
 };

This keeps runtime safe even if upstream Zod validation changes.

apps/web/utils/actions/rule.ts (1)

225-241: Scope the action update to the current rule to prevent cross-rule updates

Using update with where: { id: a.id } isn’t scoped to the current rule. A crafted payload could update an action not belonging to this rule/email account. Prefer updateMany with a guarded where clause including ruleId to ensure only actions of this rule are modified.

Apply this diff within the selected range to guard updates by ruleId:

-          ...actionsToUpdate.map((a) => {
-            return prisma.action.update({
-              where: { id: a.id },
-              data: sanitizeActionFields({
+          ...actionsToUpdate.map((a) => {
+            return prisma.action.updateMany({
+              where: { id: a.id, ruleId: id },
+              data: sanitizeActionFields({
                 type: a.type,
                 label: a.label?.value,
                 subject: a.subject?.value,
                 content: a.content?.value,
                 to: a.to?.value,
                 cc: a.cc?.value,
                 bcc: a.bcc?.value,
                 url: a.url?.value,
                 folderName: a.folderName?.value,
-                folderId: a.folderId?.value,
+                folderId: a.folderId?.value,
                 delayInMinutes: a.delayInMinutes,
               }),
             });
           }),

Additionally, tighten actionsToUpdate to only include actions that belong to currentRule (outside selected lines):

const actionsToUpdate = actions.filter(
  (a) => a.id && currentActions.some((ca) => ca.id === a.id),
);

Also applies to: 254-262

🧹 Nitpick comments (10)
apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx (1)

137-139: Adding folderId: null to action objects is correct; update ActionBadges prop type for consistency

Propagating folderId in these action shapes aligns with the repo-wide folderId introduction and prevents shape mismatches. One follow-up: ActionBadges' prop type doesn't include folderId, which can hurt type safety and DX (especially if getActionDisplay starts leveraging folderId for MOVE_FOLDER).

Update ActionBadges' actions type to include folderId (outside the selected lines):

export function ActionBadges({
  actions,
}: {
  actions: {
    id: string;
    type: ActionType;
    label?: string | null;
    folderName?: string | null;
    folderId?: string | null; // add this
  }[];
}) {
  // ...
}

Also applies to: 155-157, 174-176

apps/web/prisma/schema.prisma (1)

351-352: LGTM; consider adding indexes if you’ll query by folderId at scale

Adding folderId as optional on Action, ExecutedAction, and ScheduledAction matches the PR’s objectives. If any hot paths will query/filter by folderId (especially in ExecutedAction/ScheduledAction flows), consider adding composite indexes with emailAccountId to keep lookups fast.

If needed, augment the schema with indexes like:

model ExecutedAction {
  // ...
  folderId String?
  // ...
  @@index([folderId, executedRuleId])
}

model ScheduledAction {
  // ...
  folderId String?
  // ...
  @@index([emailAccountId, folderId, status])
}

Also applies to: 428-429, 459-460

apps/web/prisma/migrations/20250812223533_add_folder_id/migration.sql (1)

2-8: Migration is safe and minimal; add indexes in schema if needed

Adding nullable TEXT columns is a fast, lock-light operation in Postgres. If you decide to index folderId (per schema suggestion), declare indexes in schema.prisma and re-migrate so Prisma manages them.

apps/web/utils/email/microsoft.ts (1)

170-181: Consider consolidating duplicate archiving logic.

Both archiveThread and archiveThreadWithLabel contain identical logic except for the actionSource parameter. Consider refactoring to reduce duplication.

Apply this diff to consolidate the archiving functions:

-  async archiveThread(threadId: string, ownerEmail: string): Promise<void> {
-    await outlookArchiveThread({
-      client: this.client,
-      threadId,
-      ownerEmail,
-      actionSource: "automation",
-      folderId: "archive",
-    });
-  }
-
-  async archiveThreadWithLabel(
-    threadId: string,
-    ownerEmail: string,
-  ): Promise<void> {
-    await outlookArchiveThread({
-      client: this.client,
-      threadId,
-      ownerEmail,
-      actionSource: "user",
-      folderId: "archive",
-    });
-  }
+  async archiveThread(threadId: string, ownerEmail: string): Promise<void> {
+    return this._archiveWithSource(threadId, ownerEmail, "automation");
+  }
+
+  async archiveThreadWithLabel(
+    threadId: string,
+    ownerEmail: string,
+  ): Promise<void> {
+    return this._archiveWithSource(threadId, ownerEmail, "user");
+  }
+
+  private async _archiveWithSource(
+    threadId: string,
+    ownerEmail: string,
+    actionSource: "user" | "automation",
+  ): Promise<void> {
+    await outlookArchiveThread({
+      client: this.client,
+      threadId,
+      ownerEmail,
+      actionSource,
+      folderId: "archive",
+    });
+  }
apps/web/app/api/user/folders/route.ts (1)

35-37: Consider null safety for expires_at conversion.

The conversion to seconds could be simplified and made safer by handling the null case in the ternary operator itself.

Apply this diff to simplify the conversion:

-      expiresAt: emailAccount.account.expires_at
-        ? Math.floor(emailAccount.account.expires_at.getTime() / 1000)
-        : null,
+      expiresAt: emailAccount.account.expires_at?.getTime() 
+        ? Math.floor(emailAccount.account.expires_at.getTime() / 1000)
+        : null,
apps/web/utils/action-item.ts (1)

147-154: Consider updating the UI label for folder selection.

The label "Folder name" for the MOVE_FOLDER action might be misleading since users will now be selecting from a list rather than typing a folder name. Consider updating it to better reflect the new functionality.

Apply this diff to update the label:

   [ActionType.MOVE_FOLDER]: {
     fields: [
       {
         name: "folderName",
-        label: "Folder name",
+        label: "Folder",
       },
     ],
   },
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (1)

1196-1207: Consider validating folder selection immediately.

When a folder is selected or cleared, consider triggering validation immediately to provide instant feedback to the user.

Apply this diff to add immediate validation:

                       onChangeValue={(folderData) => {
                         if (folderData.name && folderData.id) {
                           setValue(`actions.${index}.folderName`, {
                             value: folderData.name,
                           });
                           setValue(`actions.${index}.folderId`, {
                             value: folderData.id,
                           });
                         } else {
                           setValue(`actions.${index}.folderName`, undefined);
                           setValue(`actions.${index}.folderId`, undefined);
                         }
+                        trigger(`actions.${index}`);
                       }}
apps/web/components/FolderCombobox.tsx (1)

44-96: Consider handling edge cases and potential improvements

The component implementation looks clean, but there are a few areas that could be improved:

  1. The component doesn't handle keyboard navigation properly when selecting folders
  2. The search filter implementation is case-insensitive but doesn't handle special characters that might cause issues
  3. The width of the popover content might not match the trigger width on all screen sizes

Consider these improvements:

 export function FolderCombobox({
   folders,
   isLoading,
   value,
   onChangeValue,
   placeholder = "Select a folder...",
 }: FolderComboboxProps) {
   const [open, setOpen] = useState(false);
   const [searchQuery, setSearchQuery] = useState("");
 
   const selectedFolder = folders.find((folder) => folder.id === value);
 
+  // Escape special regex characters in search query
+  const escapeRegex = (str: string) => 
+    str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
   const filteredFolders = folders.filter((folder) =>
-    folder.displayName.toLowerCase().includes(searchQuery.toLowerCase()),
+    folder.displayName.toLowerCase().includes(escapeRegex(searchQuery.toLowerCase())),
   );
 
   return (
     <Popover open={open} onOpenChange={setOpen}>
       <PopoverTrigger asChild>
         <Button
           variant="outline"
           role="combobox"
           aria-expanded={open}
+          aria-haspopup="listbox"
+          aria-controls="folder-listbox"
           className="w-full justify-between"
           disabled={isLoading}
         >
           {selectedFolder ? selectedFolder.displayName : placeholder}
           <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
         </Button>
       </PopoverTrigger>
-      <PopoverContent className="w-full p-0">
+      <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
         <Command>
           <div className="flex items-center border-b px-3">
             <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
             <CommandInput
               placeholder="Search..."
               value={searchQuery}
               onValueChange={setSearchQuery}
               className="border-0 focus:ring-0"
             />
           </div>
-          <CommandList>
+          <CommandList id="folder-listbox" role="listbox">
             <CommandEmpty>No folder found.</CommandEmpty>
             <CommandGroup>
               {filteredFolders.map((folder) => (
                 <CommandItem
                   key={folder.id}
                   value={folder.id}
+                  aria-selected={value === folder.id}
                   onSelect={(currentValue) => {
                     onChangeValue(currentValue === value ? "" : currentValue);
                     setOpen(false);
                   }}
                 >
apps/web/components/FolderSelector.tsx (1)

203-217: Optimize conditional rendering logic

The conditional rendering logic for the button content is complex and could be simplified. The current implementation checks selectedFolder?.displayName twice unnecessarily.

Simplify the conditional rendering:

             <div className="flex items-center gap-2 flex-1">
               {isLoading ? (
                 <>
                   <Loader2 className="h-4 w-4 animate-spin" />
                   <span>Loading folders...</span>
                 </>
-              ) : selectedFolder?.displayName ? (
+              ) : selectedFolder ? (
                 <div className="flex items-center gap-2">
                   <FolderIcon className="h-4 w-4" />
                   <span>{value.name || selectedFolder?.displayName || ""}</span>
                 </div>
               ) : (
                 placeholder
               )}
             </div>
             <div className="flex items-center gap-1">
-              {selectedFolder?.displayName && !isLoading && (
+              {selectedFolder && !isLoading && (
                 <Button
                   variant="ghost"
                   size="sm"
                   className="h-6 w-6 p-0 hover:bg-muted"
                   onClick={(e) => {
                     e.stopPropagation();
                     onChangeValue({ name: "", id: "" });
                   }}
                   title="Clear folder selection"
                 >
                   <X className="h-3 w-3" />
                 </Button>
               )}
               <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
             </div>
apps/web/utils/outlook/label.ts (1)

319-327: Inconsistent error message formatting

The error logging on lines 320-326 has inconsistent indentation in the multi-line string literal, which could make logs harder to read.

Fix the formatting:

     if (publishResult.status === "rejected") {
-      logger.error(
-        `Failed to publish action to move thread to folder ${folderId}`,
-        {
-          threadId,
-          error: publishResult.reason,
-        },
-      );
+      logger.error(`Failed to publish action to move thread to folder ${folderId}`, {
+        threadId,
+        error: publishResult.reason,
+      });
     }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe60a16 and 536543f.

📒 Files selected for processing (20)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (8 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx (3 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx (1 hunks)
  • apps/web/app/api/user/folders/route.ts (1 hunks)
  • apps/web/app/api/user/rules/[id]/route.ts (1 hunks)
  • apps/web/components/FolderCombobox.tsx (1 hunks)
  • apps/web/components/FolderSelector.tsx (1 hunks)
  • apps/web/hooks/useFolders.ts (1 hunks)
  • apps/web/prisma/migrations/20250812223533_add_folder_id/migration.sql (1 hunks)
  • apps/web/prisma/schema.prisma (3 hunks)
  • apps/web/utils/action-item.ts (6 hunks)
  • apps/web/utils/actions/rule.ts (4 hunks)
  • apps/web/utils/actions/rule.validation.ts (2 hunks)
  • apps/web/utils/ai/actions.ts (1 hunks)
  • apps/web/utils/ai/types.ts (1 hunks)
  • apps/web/utils/email/microsoft.ts (3 hunks)
  • apps/web/utils/outlook/folders.ts (1 hunks)
  • apps/web/utils/outlook/label.ts (8 hunks)
  • apps/web/utils/outlook/message.ts (0 hunks)
  • apps/web/utils/scheduled-actions/scheduler.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/utils/outlook/message.ts
🧰 Additional context used
📓 Path-based instructions (31)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use @/ for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX

Files:

  • apps/web/utils/scheduled-actions/scheduler.ts
  • apps/web/utils/ai/types.ts
  • apps/web/hooks/useFolders.ts
  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/api/user/rules/[id]/route.ts
  • apps/web/components/FolderCombobox.tsx
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/ai/actions.ts
  • apps/web/components/FolderSelector.tsx
  • apps/web/utils/email/microsoft.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/utils/outlook/label.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/utils/actions/rule.validation.ts
  • apps/web/utils/action-item.ts
!{.cursor/rules/*.mdc}

📄 CodeRabbit Inference Engine (.cursor/rules/cursor-rules.mdc)

Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location

Files:

  • apps/web/utils/scheduled-actions/scheduler.ts
  • apps/web/utils/ai/types.ts
  • apps/web/prisma/schema.prisma
  • apps/web/hooks/useFolders.ts
  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/api/user/rules/[id]/route.ts
  • apps/web/components/FolderCombobox.tsx
  • apps/web/prisma/migrations/20250812223533_add_folder_id/migration.sql
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/ai/actions.ts
  • apps/web/components/FolderSelector.tsx
  • apps/web/utils/email/microsoft.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/utils/outlook/label.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/utils/actions/rule.validation.ts
  • apps/web/utils/action-item.ts
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/form-handling.mdc)

**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod

Files:

  • apps/web/utils/scheduled-actions/scheduler.ts
  • apps/web/utils/ai/types.ts
  • apps/web/hooks/useFolders.ts
  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/api/user/rules/[id]/route.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/ai/actions.ts
  • apps/web/utils/email/microsoft.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/utils/outlook/label.ts
  • apps/web/utils/actions/rule.validation.ts
  • apps/web/utils/action-item.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/logging.mdc)

**/*.{ts,tsx}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import Prisma in the project using import prisma from "@/utils/prisma";

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.

Files:

  • apps/web/utils/scheduled-actions/scheduler.ts
  • apps/web/utils/ai/types.ts
  • apps/web/hooks/useFolders.ts
  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/api/user/rules/[id]/route.ts
  • apps/web/components/FolderCombobox.tsx
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/ai/actions.ts
  • apps/web/components/FolderSelector.tsx
  • apps/web/utils/email/microsoft.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/utils/outlook/label.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/utils/actions/rule.validation.ts
  • apps/web/utils/action-item.ts
apps/web/utils/**

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

Create utility functions in utils/ folder for reusable logic

Files:

  • apps/web/utils/scheduled-actions/scheduler.ts
  • apps/web/utils/ai/types.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/ai/actions.ts
  • apps/web/utils/email/microsoft.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/utils/outlook/label.ts
  • apps/web/utils/actions/rule.validation.ts
  • apps/web/utils/action-item.ts
apps/web/utils/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size

Files:

  • apps/web/utils/scheduled-actions/scheduler.ts
  • apps/web/utils/ai/types.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/ai/actions.ts
  • apps/web/utils/email/microsoft.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/utils/outlook/label.ts
  • apps/web/utils/actions/rule.validation.ts
  • apps/web/utils/action-item.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use elements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...

Files:

  • apps/web/utils/scheduled-actions/scheduler.ts
  • apps/web/utils/ai/types.ts
  • apps/web/hooks/useFolders.ts
  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/api/user/rules/[id]/route.ts
  • apps/web/components/FolderCombobox.tsx
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/ai/actions.ts
  • apps/web/components/FolderSelector.tsx
  • apps/web/utils/email/microsoft.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/utils/outlook/label.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/utils/actions/rule.validation.ts
  • apps/web/utils/action-item.ts
!pages/_document.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.

Files:

  • apps/web/utils/scheduled-actions/scheduler.ts
  • apps/web/utils/ai/types.ts
  • apps/web/prisma/schema.prisma
  • apps/web/hooks/useFolders.ts
  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/api/user/rules/[id]/route.ts
  • apps/web/components/FolderCombobox.tsx
  • apps/web/prisma/migrations/20250812223533_add_folder_id/migration.sql
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/ai/actions.ts
  • apps/web/components/FolderSelector.tsx
  • apps/web/utils/email/microsoft.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/utils/outlook/label.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/utils/actions/rule.validation.ts
  • apps/web/utils/action-item.ts
apps/web/utils/{ai,llms}/**/*

📄 CodeRabbit Inference Engine (.cursor/rules/llm.mdc)

LLM-related code must be organized in the directories: apps/web/utils/ai/, apps/web/utils/llms/, and apps/web/tests/ for LLM-specific tests.

Files:

  • apps/web/utils/ai/types.ts
  • apps/web/utils/ai/actions.ts
apps/web/utils/{ai,llms}/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/llm.mdc)

apps/web/utils/{ai,llms}/**/*.ts: Keep system prompts and user prompts separate in LLM-related functions.
System prompt should define the LLM's role and task specifications.
User prompt should contain the actual data and context.
Always define a Zod schema for response validation in LLM-related functions.
Make Zod schemas as specific as possible to guide the LLM output.
Use descriptive scoped loggers for each LLM feature.
Log inputs and outputs with appropriate log levels in LLM-related functions.
Include relevant context in log messages for LLM-related code.
Implement early returns for invalid inputs in LLM-related functions.
Use proper error types and logging in LLM-related code.
Implement fallbacks for AI failures in LLM-related functions.
Add retry logic for transient failures using withRetry in LLM-related functions.
Use XML-like tags to structure data in LLM prompts.
Remove excessive whitespace and truncate long inputs in LLM prompts.
Format data consistently across similar LLM-related functions.
Use TypeScript types for all parameters and return values in LLM-related functions.
Define clear interfaces for complex input/output structures in LLM-related code.
Keep related AI functions in the same file or directory.
Extract common patterns into utility functions in LLM-related code.
Document complex AI logic with clear comments in LLM-related code.

Files:

  • apps/web/utils/ai/types.ts
  • apps/web/utils/ai/actions.ts
apps/web/prisma/schema.prisma

📄 CodeRabbit Inference Engine (.cursor/rules/prisma.mdc)

The Prisma schema file must be located at apps/web/prisma/schema.prisma

Files:

  • apps/web/prisma/schema.prisma
apps/web/hooks/**/*.ts

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

Use SWR for efficient data fetching and caching

apps/web/hooks/**/*.ts: Use SWR for client-side data fetching and caching.
Call mutate() after successful mutations to refresh SWR data on the client.

Files:

  • apps/web/hooks/useFolders.ts
apps/web/hooks/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

Call mutate() after successful mutations to refresh data

Files:

  • apps/web/hooks/useFolders.ts
apps/web/hooks/**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/hooks.mdc)

Place custom hooks in the apps/web/hooks/ directory.

Files:

  • apps/web/hooks/useFolders.ts
apps/web/hooks/use*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/hooks.mdc)

apps/web/hooks/use*.{js,jsx,ts,tsx}: Name custom hooks with the use prefix (e.g., useAccounts.ts).
For fetching data from API endpoints in custom hooks, prefer using useSWR.
Create dedicated hooks for specific data types (e.g., useAccounts, useLabels).
Custom hooks should encapsulate reusable stateful logic, especially for data fetching or complex UI interactions.
Keep custom hooks focused on a single responsibility.

Files:

  • apps/web/hooks/useFolders.ts
apps/web/app/**

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

NextJS app router structure with (app) directory

Files:

  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/api/user/rules/[id]/route.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/app/api/**/route.ts

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/app/api/**/route.ts: Use withAuth for user-level operations
Use withEmailAccount for email-account-level operations
Do NOT use POST API routes for mutations - use server actions instead
No need for try/catch in GET routes when using middleware
Export response types from GET routes

apps/web/app/api/**/route.ts: Wrap all GET API route handlers with withAuth or withEmailAccount middleware for authentication and authorization.
Export response types from GET API routes for type-safe client usage.
Do not use try/catch in GET API routes when using authentication middleware; rely on centralized error handling.

Files:

  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/api/user/rules/[id]/route.ts
**/api/**/route.ts

📄 CodeRabbit Inference Engine (.cursor/rules/security.mdc)

**/api/**/route.ts: ALL API routes that handle user data MUST use appropriate authentication and authorization middleware (withAuth or withEmailAccount).
ALL database queries in API routes MUST be scoped to the authenticated user/account (e.g., include userId or emailAccountId in query filters).
Always validate that resources belong to the authenticated user before performing operations (resource ownership validation).
Use withEmailAccount middleware for API routes that operate on a specific email account (i.e., use or require emailAccountId).
Use withAuth middleware for API routes that operate at the user level (i.e., use or require only userId).
Use withError middleware (with proper validation) for public endpoints, custom authentication, or cron endpoints.
Cron endpoints MUST use withError middleware and validate the cron secret using hasCronSecret(request) or hasPostCronSecret(request).
Cron endpoints MUST capture unauthorized attempts with captureException and return a 401 status for unauthorized requests.
All parameters in API routes MUST be validated for type, format, and length before use.
Request bodies in API routes MUST be validated using Zod schemas before use.
All Prisma queries in API routes MUST only return necessary fields and never expose sensitive data.
Error messages in API routes MUST not leak internal information or sensitive data; use generic error messages and SafeError where appropriate.
API routes MUST use a consistent error response format, returning JSON with an error message and status code.
All findUnique and findFirst Prisma calls in API routes MUST include ownership filters (e.g., userId or emailAccountId).
All findMany Prisma calls in API routes MUST be scoped to the authenticated user's data.
Never use direct object references in API routes without ownership checks (prevent IDOR vulnerabilities).
Prevent mass assignment vulnerabilities by only allowing explicitly whitelisted fields in update operations in AP...

Files:

  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/api/user/rules/[id]/route.ts
apps/web/app/api/**/*.{ts,js}

📄 CodeRabbit Inference Engine (.cursor/rules/security-audit.mdc)

apps/web/app/api/**/*.{ts,js}: All API route handlers in 'apps/web/app/api/' must use authentication middleware: withAuth, withEmailAccount, or withError (with custom authentication logic).
All Prisma queries in API routes must include user/account filtering (e.g., emailAccountId or userId in WHERE clauses) to prevent unauthorized data access.
All parameters used in API routes must be validated before use; do not use parameters from 'params' or request bodies directly in queries without validation.
Request bodies in API routes should use Zod schemas for validation.
API routes should only return necessary fields using Prisma's 'select' and must not include sensitive data in error messages.
Error messages in API routes must not reveal internal details; use generic errors and SafeError for user-facing errors.
All QStash endpoints (API routes called via publishToQstash or publishToQstashQueue) must use verifySignatureAppRouter to verify request authenticity.
All cron endpoints in API routes must use hasCronSecret or hasPostCronSecret for authentication.
Do not hardcode weak or plaintext secrets in API route files; secrets must not be directly assigned as string literals.
Review all new withError usage in API routes to ensure custom authentication is implemented where required.

Files:

  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/api/user/rules/[id]/route.ts
apps/web/**/*.tsx

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/**/*.tsx: Follow tailwindcss patterns with prettier-plugin-tailwindcss
Prefer functional components with hooks
Use shadcn/ui components when available
Ensure responsive design with mobile-first approach
Follow consistent naming conventions (PascalCase for components)
Use LoadingContent component for async data
Use result?.serverError with toastError and toastSuccess
Use LoadingContent component to handle loading and error states consistently
Pass loading, error, and children props to LoadingContent

Files:

  • apps/web/components/FolderCombobox.tsx
  • apps/web/components/FolderSelector.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/components/**/*.tsx

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

Use React Hook Form with Zod validation for form handling

Use the LoadingContent component to handle loading and error states consistently in data-fetching components.

Use PascalCase for components (e.g. components/Button.tsx)

Files:

  • apps/web/components/FolderCombobox.tsx
  • apps/web/components/FolderSelector.tsx
**/*.tsx

📄 CodeRabbit Inference Engine (.cursor/rules/form-handling.mdc)

**/*.tsx: Use React Hook Form with Zod for validation
Validate form inputs before submission
Show validation errors inline next to form fields

Files:

  • apps/web/components/FolderCombobox.tsx
  • apps/web/components/FolderSelector.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{jsx,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx}: Don't destructure props inside JSX components in Solid projects.
Don't use both children and dangerouslySetInnerHTML props on the same element.
Don't use Array index in keys.
Don't assign to React component props.
Don't define React components inside other components.
Don't use event handlers on non-interactive elements.
Don't assign JSX properties multiple times.
Don't add extra closing tags for components without children.
Use <>...</> instead of ....
Don't insert comments as text nodes.
Don't use the return value of React.render.
Make sure all dependencies are correctly specified in React hooks.
Make sure all React hooks are called from the top level of component functions.
Don't use unnecessary fragments.
Don't pass children as props.
Use semantic elements instead of role attributes in JSX.

Files:

  • apps/web/components/FolderCombobox.tsx
  • apps/web/components/FolderSelector.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{html,jsx,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{html,jsx,tsx}: Don't use or elements.
Don't use accessKey attribute on any HTML element.
Don't set aria-hidden="true" on focusable elements.
Don't add ARIA roles, states, and properties to elements that don't support them.
Only use the scope prop on elements.
Don't assign non-interactive ARIA roles to interactive HTML elements.
Make sure label elements have text content and are associated with an input.
Don't assign interactive ARIA roles to non-interactive HTML elements.
Don't assign tabIndex to non-interactive HTML elements.
Don't use positive integers for tabIndex property.
Don't include "image", "picture", or "photo" in img alt prop.
Don't use explicit role property that's the same as the implicit/default role.
Make static elements with click handlers use a valid role attribute.
Always include a title element for SVG elements.
Give all elements requiring alt text meaningful information for screen readers.
Make sure anchors have content that's accessible to screen readers.
Assign tabIndex to non-interactive HTML elements with aria-activedescendant.
Include all required ARIA attributes for elements with ARIA roles.
Make sure ARIA properties are valid for the element's supported roles.
Always include a type attribute for button elements.
Make elements with interactive roles and handlers focusable.
Give heading elements content that's accessible to screen readers (not hidden with aria-hidden).
Always include a lang attribute on the html element.
Always include a title attribute for iframe elements.
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress.
Accompany onMouseOver/onMouseOut with onFocus/onBlur.
Include caption tracks for audio and video elements.
Make sure all anchors are valid and navigable.
Ensure all ARIA properties (aria-*) are valid.
Use valid, non-abstract ARIA roles for elements with ARIA roles.
Use valid ARIA state and property values.
Use valid values for the autocomplete attribute on input eleme...

Files:

  • apps/web/components/FolderCombobox.tsx
  • apps/web/components/FolderSelector.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/utils/actions/**/*.ts

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/utils/actions/**/*.ts: Use server actions for all mutations (create/update/delete operations)
next-safe-action provides centralized error handling
Use Zod schemas for validation on both client and server
Use revalidatePath in server actions for cache invalidation

apps/web/utils/actions/**/*.ts: Use server actions (with next-safe-action) for all mutations (create/update/delete operations); do NOT use POST API routes for mutations.
Use revalidatePath in server actions to invalidate cache after mutations.

Files:

  • apps/web/utils/actions/rule.ts
  • apps/web/utils/actions/rule.validation.ts
apps/web/utils/actions/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/server-actions.mdc)

apps/web/utils/actions/*.ts: Implement all server actions using the next-safe-action library for type safety, input validation, context management, and error handling. Refer to apps/web/utils/actions/safe-action.ts for client definitions (actionClient, actionClientUser, adminActionClient).
Use actionClientUser when only authenticated user context (userId) is needed.
Use actionClient when both authenticated user context and a specific emailAccountId are needed. The emailAccountId must be bound when calling the action from the client.
Use adminActionClient for actions restricted to admin users.
Access necessary context (like userId, emailAccountId, etc.) provided by the safe action client via the ctx object in the .action() handler.
Server Actions are strictly for mutations (operations that change data, e.g., creating, updating, deleting). Do NOT use Server Actions for data fetching (GET operations). For data fetching, use dedicated GET API Routes combined with SWR Hooks.
Use SafeError for expected/handled errors within actions if needed. next-safe-action provides centralized error handling.
Use the .metadata({ name: "actionName" }) method to provide a meaningful name for monitoring. Sentry instrumentation is automatically applied via withServerActionInstrumentation within the safe action clients.
If an action modifies data displayed elsewhere, use revalidatePath or revalidateTag from next/cache within the action handler as needed.

Server action files must start with use server

Files:

  • apps/web/utils/actions/rule.ts
  • apps/web/utils/actions/rule.validation.ts
apps/web/app/(app)/*/**

📄 CodeRabbit Inference Engine (.cursor/rules/page-structure.mdc)

Components for the page are either put in page.tsx, or in the apps/web/app/(app)/PAGE_NAME folder

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/app/(app)/*/**/*.tsx

📄 CodeRabbit Inference Engine (.cursor/rules/page-structure.mdc)

If you need to use onClick in a component, that component is a client component and file must start with 'use client'

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/app/(app)/*/**/**/*.tsx

📄 CodeRabbit Inference Engine (.cursor/rules/page-structure.mdc)

If we're in a deeply nested component we will use swr to fetch via API

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/app/**/*.tsx

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

Components with onClick must be client components with use client directive

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/utils/actions/*.validation.ts

📄 CodeRabbit Inference Engine (.cursor/rules/fullstack-workflow.mdc)

Define Zod schemas for validation in dedicated files and use them for both client and server validation.

Define input validation schemas using Zod in the corresponding .validation.ts file. These schemas are used by next-safe-action (.schema()) and can also be reused on the client for form validation.

Files:

  • apps/web/utils/actions/rule.validation.ts
🧠 Learnings (16)
📚 Learning: 2025-07-18T15:05:41.705Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/hooks.mdc:0-0
Timestamp: 2025-07-18T15:05:41.705Z
Learning: Applies to apps/web/hooks/use*.{js,jsx,ts,tsx} : Create dedicated hooks for specific data types (e.g., `useAccounts`, `useLabels`).

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-18T15:04:30.467Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-07-18T15:04:30.467Z
Learning: Applies to apps/web/hooks/**/*.ts : Use SWR for efficient data fetching and caching

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-20T09:00:16.505Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/project-structure.mdc:0-0
Timestamp: 2025-07-20T09:00:16.505Z
Learning: Use `swr` for data fetching in deeply nested components

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-18T15:05:56.644Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/index.mdc:0-0
Timestamp: 2025-07-18T15:05:56.644Z
Learning: Fetching data from the API using SWR

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-18T15:04:30.467Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-07-18T15:04:30.467Z
Learning: Applies to apps/web/app/api/**/route.ts : Use `withEmailAccount` for email-account-level operations

Applied to files:

  • apps/web/app/api/user/folders/route.ts
📚 Learning: 2025-07-18T17:27:46.389Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/security.mdc:0-0
Timestamp: 2025-07-18T17:27:46.389Z
Learning: Applies to **/api/**/route.ts : Use `withEmailAccount` middleware for API routes that operate on a specific email account (i.e., use or require `emailAccountId`).

Applied to files:

  • apps/web/app/api/user/folders/route.ts
📚 Learning: 2025-07-18T15:05:16.146Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-07-18T15:05:16.146Z
Learning: Applies to apps/web/app/api/**/route.ts : Wrap all GET API route handlers with `withAuth` or `withEmailAccount` middleware for authentication and authorization.

Applied to files:

  • apps/web/app/api/user/folders/route.ts
📚 Learning: 2025-07-18T15:05:26.713Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/get-api-route.mdc:0-0
Timestamp: 2025-07-18T15:05:26.713Z
Learning: Applies to app/api/**/route.ts : Always wrap the handler with `withAuth` or `withEmailAccount` for consistent error handling and authentication in GET API routes.

Applied to files:

  • apps/web/app/api/user/folders/route.ts
📚 Learning: 2025-07-18T15:05:34.899Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/gmail-api.mdc:0-0
Timestamp: 2025-07-18T15:05:34.899Z
Learning: Applies to apps/web/utils/gmail/**/*.ts : Keep provider-specific implementation details isolated in the appropriate utils subfolder (e.g., 'apps/web/utils/gmail/')

Applied to files:

  • apps/web/utils/email/microsoft.ts
  • apps/web/utils/outlook/folders.ts
📚 Learning: 2025-07-18T15:05:16.146Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-07-18T15:05:16.146Z
Learning: Applies to apps/web/utils/actions/*.validation.ts : Define Zod schemas for validation in dedicated files and use them for both client and server validation.

Applied to files:

  • apps/web/utils/actions/rule.validation.ts
📚 Learning: 2025-07-18T17:27:58.249Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/server-actions.mdc:0-0
Timestamp: 2025-07-18T17:27:58.249Z
Learning: Applies to apps/web/utils/actions/*.validation.ts : Define input validation schemas using Zod in the corresponding `.validation.ts` file. These schemas are used by `next-safe-action` (`.schema()`) and can also be reused on the client for form validation.

Applied to files:

  • apps/web/utils/actions/rule.validation.ts
📚 Learning: 2025-07-18T15:04:30.467Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-07-18T15:04:30.467Z
Learning: Applies to apps/web/utils/actions/**/*.ts : Use Zod schemas for validation on both client and server

Applied to files:

  • apps/web/utils/actions/rule.validation.ts
📚 Learning: 2025-08-10T22:08:49.243Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-08-10T22:08:49.243Z
Learning: Applies to apps/web/utils/{ai,llms}/**/*.ts : Make Zod schemas as specific as possible to guide the LLM output.

Applied to files:

  • apps/web/utils/actions/rule.validation.ts
📚 Learning: 2025-07-18T15:04:57.115Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-07-18T15:04:57.115Z
Learning: Applies to **/*.ts : Define validation schemas using Zod

Applied to files:

  • apps/web/utils/actions/rule.validation.ts
📚 Learning: 2025-08-10T22:08:49.243Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-08-10T22:08:49.243Z
Learning: Applies to apps/web/utils/{ai,llms}/**/*.ts : Always define a Zod schema for response validation in LLM-related functions.

Applied to files:

  • apps/web/utils/actions/rule.validation.ts
📚 Learning: 2025-07-20T09:00:41.968Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/security-audit.mdc:0-0
Timestamp: 2025-07-20T09:00:41.968Z
Learning: Applies to apps/web/app/api/**/*.{ts,js} : Request bodies in API routes should use Zod schemas for validation.

Applied to files:

  • apps/web/utils/actions/rule.validation.ts
🧬 Code Graph Analysis (7)
apps/web/hooks/useFolders.ts (1)
apps/web/utils/outlook/folders.ts (1)
  • OutlookFolder (9-14)
apps/web/app/api/user/folders/route.ts (4)
apps/web/app/api/user/rules/[id]/route.ts (1)
  • GET (51-60)
apps/web/utils/middleware.ts (1)
  • withEmailAccount (251-255)
apps/web/utils/outlook/client.ts (1)
  • getOutlookClientWithRefresh (76-148)
apps/web/utils/outlook/folders.ts (1)
  • getOutlookFolders (16-73)
apps/web/components/FolderCombobox.tsx (3)
apps/web/utils/outlook/folders.ts (1)
  • OutlookFolder (9-14)
apps/web/components/Button.tsx (1)
  • Button (60-87)
apps/web/utils/index.ts (1)
  • cn (4-6)
apps/web/components/FolderSelector.tsx (3)
apps/web/utils/outlook/folders.ts (2)
  • OutlookFolder (9-14)
  • FOLDER_SEPARATOR (8-8)
apps/web/utils/index.ts (1)
  • cn (4-6)
apps/web/components/Button.tsx (1)
  • Button (60-87)
apps/web/utils/outlook/folders.ts (3)
apps/web/app/api/outlook/webhook/logger.ts (1)
  • logger (3-3)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-65)
apps/web/utils/outlook/client.ts (1)
  • OutlookClient (19-63)
apps/web/utils/outlook/label.ts (2)
apps/web/utils/outlook/client.ts (1)
  • OutlookClient (19-63)
apps/web/utils/outlook/message.ts (1)
  • WELL_KNOWN_FOLDERS (14-21)
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (2)
apps/web/hooks/useFolders.ts (1)
  • useFolders (4-8)
apps/web/components/FolderSelector.tsx (1)
  • FolderSelector (88-280)
🔇 Additional comments (13)
apps/web/utils/scheduled-actions/scheduler.ts (1)

67-69: Persisting folderName/folderId in scheduled actions — verify end-to-end usage

Storing both fields here is consistent with the schema and broader PR. Please confirm:

  • Prisma ScheduledAction model has folderName/folderId as nullable fields.
  • The executor path that reads ScheduledAction (e.g., /api/scheduled-actions/execute) reads and honors folderId for MOVE_FOLDER.

If you want, I can scan the repository for the execute path and verify it reads these fields.

apps/web/app/api/user/rules/[id]/route.ts (1)

41-43: Adding folderId to action serialization — LGTM

This aligns the API shape with the new folderId-based flow. The nullability will naturally flow through as { value: null } for older rules.

apps/web/utils/ai/types.ts (1)

25-26: Extending ActionItem with folderName/folderId — LGTM

Typing against ExecutedAction keeps these fields consistent with Prisma.

apps/web/utils/actions/rule.ts (2)

259-260: folderId propagation on createMany during update: LGTM

The create path correctly forwards folderId through sanitizeActionFields. This keeps create/update parity.


91-105: folderId propagation verified end-to-end

All layers now include folderId, so creation and validation are consistent:

  • apps/web/utils/actions/rule.ts: maps folderId into sanitizeActionFields
  • apps/web/utils/actions/rule.validation.ts: folderId is in the Zod schema and checked in superRefine
  • apps/web/utils/action-item.ts: sanitizer preserves and handles folderId
  • apps/web/app/api/user/rules/[id]/route.ts: request mapping includes folderId

No further changes needed.

apps/web/app/(app)/[emailAccountId]/assistant/settings/DraftReplies.tsx (1)

92-93: LGTM: optimistic action shape now includes folderId

Adding folderId: null keeps the optimistic DRAFT_EMAIL action shape consistent with the updated schema and UI contracts.

apps/web/utils/email/microsoft.ts (2)

160-168: LGTM! Proper implementation of folderId-based archiving.

The archiving functions now correctly use folderId: "archive" instead of folder names, aligning with the PR's broader shift to folderId-based operations. The use of the well-known folder ID "archive" is appropriate for Microsoft's Graph API.


823-835: ArchiveThread correctly handles both well-known and custom folder IDs

The Outlook archiveThread implementation in apps/web/utils/outlook/label.ts (lines 233–241) already:

  • Treats well-known folder names (from WELL_KNOWN_FOLDERS) as trusted.
  • For other folderIds, issues a GET to /me/mailFolders/{folderId} to verify the folder exists before proceeding.
  • Uses the validated folderId when moving each message in the thread.

No changes needed.

apps/web/app/api/user/folders/route.ts (1)

16-44: Add error handling for missing email account.

The function should handle the case where the email account is not found to prevent potential null pointer exceptions.

Apply this diff to add proper error handling:

 async function getFolders({ emailAccountId }: { emailAccountId: string }) {
   const emailAccount = await prisma.emailAccount.findUnique({
     where: { id: emailAccountId },
     select: {
       account: {
         select: {
           provider: true,
           access_token: true,
           refresh_token: true,
           expires_at: true,
         },
       },
     },
   });
 
+  if (!emailAccount?.account) {
+    console.error(`Email account not found: ${emailAccountId}`);
+    return [];
+  }
+
   if (emailAccount?.account?.provider === "microsoft") {

Likely an incorrect or invalid review comment.

apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (1)

1047-1059: LGTM! Appropriate handling of folder fields for variables.

The logic correctly prevents the use of variables in folder-related fields, which makes sense since these are selected from a predefined list rather than being dynamically generated.

apps/web/utils/outlook/folders.ts (2)

1-5: LGTM! Logger initialization follows best practices

The scoped logger is correctly initialized at the module level as per the coding guidelines.


6-8: Good choice for the folder separator constant

The U+2999 character (⦙) is indeed an uncommon character that's unlikely to appear in typical folder names, making it a solid choice for a separator. The comment clearly explains the rationale.

apps/web/utils/outlook/label.ts (1)

238-245: Good migration to folderId-based approach

The refactoring from folderName to folderId aligns well with the broader PR objectives and provides a more reliable way to reference folders. The default value of "archive" maintains backward compatibility.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
apps/web/utils/outlook/folders.ts (3)

20-33: Harden Graph calls: add try/catch and validate shape with Zod

Uncaught errors here will bubble up and Zod-validated shapes help maintain strict typing (and avoid implicit any on response.value). This also aligns with the project’s “Define validation schemas using Zod” guidance.

Apply within this range:

-  const getFolders = async (path: string) => {
-    const fields = "id,displayName";
-    const response = await client
-      .getClient()
-      .api(path)
-      .select(fields)
-      .expand(
-        `childFolders($select=${fields};$expand=childFolders($select=${fields}))`,
-      )
-      .get();
-
-    return response.value;
-  };
+  const getFolders = async (path: string) => {
+    const fields = "id,displayName";
+    try {
+      const response = await client
+        .getClient()
+        .api(path)
+        .select(fields)
+        .expand(
+          `childFolders($select=${fields};$expand=childFolders($select=${fields}))`,
+        )
+        .get();
+      const parsed = GraphResponseSchema.parse(response);
+      return parsed.value;
+    } catch (error) {
+      logger.error("Failed to fetch folders", {
+        path,
+        message: error instanceof Error ? error.message : String(error),
+      });
+      throw error;
+    }
+  };

And add these definitions outside the selected range (place after imports):

// Graph API folder shape and validation
export type GraphApiFolder = {
  id: string;
  displayName: string;
  childFolders?: GraphApiFolder[];
};

const GraphApiFolderSchema: z.ZodType<GraphApiFolder> = z.lazy(() =>
  z.object({
    id: z.string(),
    displayName: z.string(),
    childFolders: z.array(z.lazy(() => GraphApiFolderSchema)).optional(),
  }),
);

const GraphResponseSchema = z.object({
  value: z.array(GraphApiFolderSchema),
});

const toOutlookFolder = (
  node: GraphApiFolder,
  parentId?: string,
): OutlookFolder => ({
  id: node.id,
  displayName: node.displayName,
  parentFolderId: parentId,
  childFolders: node.childFolders?.map((c) => toOutlookFolder(c, node.id)),
});

35-46: Fix typing in processFolders and leverage a normalizer

You’re treating raw Graph objects as OutlookFolder. Normalize explicitly from GraphApiFolder to OutlookFolder.

-  const processFolders = (
-    folderList: OutlookFolder[],
-    parentId?: string,
-  ): OutlookFolder[] => {
-    return folderList.map((folder) => ({
-      ...folder,
-      parentFolderId: parentId,
-      childFolders: folder.childFolders
-        ? processFolders(folder.childFolders, folder.id)
-        : undefined,
-    }));
-  };
+  const processFolders = (
+    folderList: GraphApiFolder[],
+    parentId?: string,
+  ): OutlookFolder[] => folderList.map((f) => toOutlookFolder(f, parentId));

50-74: Avoid redundant re-fetches and batch deeper calls; preserve nested children returned by $expand

  • Only fetch when childFolders is undefined (treat empty array as “fetched, no children”) to prevent repeated calls on every pass.
  • Batch the per-level fetches with Promise.all to reduce total latency. Consider adding a concurrency cap later if rate-limits become an issue.
  • When fetching, keep any nested childFolders already returned by the Graph $expand by reusing processFolders.
-    const fetchNested = async (folderList: OutlookFolder[]) => {
-      for (const folder of folderList) {
-        if (!folder.childFolders || folder.childFolders.length === 0) {
-          try {
-            const childFolders = await getFolders(
-              `/me/mailFolders/${folder.id}/childFolders`,
-            );
-            folder.childFolders = childFolders.map(
-              (childFolder: OutlookFolder) => ({
-                id: childFolder.id,
-                displayName: childFolder.displayName,
-                parentFolderId: folder.id,
-              }),
-            );
-          } catch (error) {
-            logger.warn("Failed to fetch deeper folders", {
-              folderId: folder.id,
-              error,
-            });
-          }
-        } else {
-          await fetchNested(folder.childFolders);
-        }
-      }
-    };
+    const fetchNested = async (folderList: OutlookFolder[]): Promise<void> => {
+      const tasks: Array<Promise<void>> = [];
+      for (const folder of folderList) {
+        if (folder.childFolders === undefined) {
+          tasks.push(
+            (async () => {
+              try {
+                const childFolders = await getFolders(
+                  `/me/mailFolders/${folder.id}/childFolders`,
+                );
+                // Preserve any nested children surfaced via $expand
+                folder.childFolders = processFolders(childFolders, folder.id);
+              } catch (error) {
+                logger.warn("Failed to fetch deeper folders", {
+                  folderId: folder.id,
+                  message: error instanceof Error ? error.message : String(error),
+                });
+                // Mark as fetched to avoid repeated retries on subsequent passes
+                folder.childFolders = [];
+              }
+            })(),
+          );
+        } else if (folder.childFolders.length > 0) {
+          tasks.push(fetchNested(folder.childFolders));
+        }
+      }
+      await Promise.all(tasks);
+    };
🧹 Nitpick comments (3)
apps/web/utils/outlook/folders.ts (3)

18-18: Clarify the expandLevels comment

The current comment is misleading. It does not necessarily “fetch 6 levels deep” as written.

-  expandLevels = 4, // This is fetching 6 levels deep
+  expandLevels = 4, // Additional fetch passes after the initial 2-level $expand

65-69: Log actionable error details instead of raw Error objects

The logger stringifies objects; raw Error serializes to "{}". Log message (and optionally code) for usefulness.

-            logger.warn("Failed to fetch deeper folders", {
-              folderId: folder.id,
-              error,
-            });
+            logger.warn("Failed to fetch deeper folders", {
+              folderId: folder.id,
+              message: error instanceof Error ? error.message : String(error),
+            });

16-80: Optional: handle 429 rate limits with Retry-After and cap concurrency

Microsoft Graph can throttle with 429. Consider reading Retry-After and backing off; also introduce a small concurrency limiter (e.g., p-limit) if you hit tenant-wide throttles.

If you want, I can propose a minimal p-limit integration with a safe default concurrency (e.g., 4–8).

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 536543f and 760f111.

📒 Files selected for processing (3)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (8 hunks)
  • apps/web/components/FolderSelector.tsx (1 hunks)
  • apps/web/utils/outlook/folders.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/components/FolderSelector.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
🧰 Additional context used
📓 Path-based instructions (8)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use @/ for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX

Files:

  • apps/web/utils/outlook/folders.ts
!{.cursor/rules/*.mdc}

📄 CodeRabbit Inference Engine (.cursor/rules/cursor-rules.mdc)

Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location

Files:

  • apps/web/utils/outlook/folders.ts
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/form-handling.mdc)

**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod

Files:

  • apps/web/utils/outlook/folders.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/logging.mdc)

**/*.{ts,tsx}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import Prisma in the project using import prisma from "@/utils/prisma";

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.

Files:

  • apps/web/utils/outlook/folders.ts
apps/web/utils/**

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

Create utility functions in utils/ folder for reusable logic

Files:

  • apps/web/utils/outlook/folders.ts
apps/web/utils/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size

Files:

  • apps/web/utils/outlook/folders.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use elements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...

Files:

  • apps/web/utils/outlook/folders.ts
!pages/_document.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.

Files:

  • apps/web/utils/outlook/folders.ts
🧠 Learnings (1)
📚 Learning: 2025-07-18T15:05:34.899Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/gmail-api.mdc:0-0
Timestamp: 2025-07-18T15:05:34.899Z
Learning: Applies to apps/web/utils/gmail/**/*.ts : Keep provider-specific implementation details isolated in the appropriate utils subfolder (e.g., 'apps/web/utils/gmail/')

Applied to files:

  • apps/web/utils/outlook/folders.ts
🧬 Code Graph Analysis (1)
apps/web/utils/outlook/folders.ts (3)
apps/web/app/api/outlook/webhook/logger.ts (1)
  • logger (3-3)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-65)
apps/web/utils/outlook/client.ts (1)
  • OutlookClient (19-63)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Static Code Analysis Js
  • GitHub Check: Jit Security
🔇 Additional comments (1)
apps/web/utils/outlook/folders.ts (1)

6-8: LGTM: separator choice is robust

Good call using a rarely-used Unicode char to avoid collisions with user-defined folder names.

@edulelis edulelis changed the title WIP: Refactor folder functionality Refactor folder functionality Aug 13, 2025
@edulelis edulelis changed the title Refactor folder functionality Refactor Outlook folder functionality Aug 13, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/web/utils/outlook/label.ts (1)

246-253: Add validation for empty or malformed folderId values.

The current validation only checks if the folderId exists but doesn't validate if it's empty, whitespace-only, or malformed, which could lead to unnecessary API calls.

Add validation for empty or invalid folderIds:

 export async function archiveThread({
   client,
   threadId,
   ownerEmail,
   actionSource,
   folderId = "archive",
 }: {
   client: OutlookClient;
   threadId: string;
   ownerEmail: string;
   actionSource: TinybirdEmailAction["actionSource"];
   folderId?: string;
 }) {
-  if (!folderId) {
+  // Validate folderId
+  if (!folderId || typeof folderId !== 'string' || !folderId.trim()) {
+    logger.warn("Invalid or empty folderId provided", { folderId, threadId });
+    return;
+  }
+
+  if (!folderId) {
     logger.warn("No folderId provided, skipping archive operation", {
       threadId,
       ownerEmail,
       actionSource,
     });
     return;
   }
🧹 Nitpick comments (1)
apps/web/utils/outlook/label.ts (1)

255-271: Consider caching folder existence checks for performance.

The current implementation makes an API call to verify custom folder existence on every archive operation. For frequently used custom folders, this could be optimized with caching.

Consider implementing a simple in-memory cache for folder existence:

+// Simple cache for folder existence (could be moved to a shared location)
+const folderExistenceCache = new Map<string, boolean>();
+const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
+const cacheTimestamps = new Map<string, number>();

 // Check if the destination folder exists (only for custom folders, well-known names can be trusted and used directly)
 const wellKnownFolders = Object.keys(WELL_KNOWN_FOLDERS);
 if (!wellKnownFolders.includes(folderId.toLowerCase())) {
+  const now = Date.now();
+  const cacheKey = `${ownerEmail}:${folderId}`;
+  const cached = folderExistenceCache.get(cacheKey);
+  const cacheTime = cacheTimestamps.get(cacheKey);
+  
+  if (cached !== undefined && cacheTime && (now - cacheTime) < CACHE_TTL) {
+    if (!cached) {
+      logger.warn(
+        "Custom destination folder not found (cached), skipping archive operation",
+        { folderId, threadId }
+      );
+      return;
+    }
+  } else {
     try {
       await client.getClient().api(`/me/mailFolders/${folderId}`).get();
+      folderExistenceCache.set(cacheKey, true);
+      cacheTimestamps.set(cacheKey, now);
     } catch (error) {
+      folderExistenceCache.set(cacheKey, false);
+      cacheTimestamps.set(cacheKey, now);
       logger.warn(
         "Custom destination folder not found, skipping archive operation",
         {
           folderId,
           threadId,
           error,
         },
       );
       return;
     }
+  }
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 760f111 and 2cf14f6.

📒 Files selected for processing (4)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (8 hunks)
  • apps/web/components/FolderSelector.tsx (1 hunks)
  • apps/web/hooks/useFolders.ts (1 hunks)
  • apps/web/utils/outlook/label.ts (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/hooks/useFolders.ts
  • apps/web/components/FolderSelector.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
🧰 Additional context used
📓 Path-based instructions (8)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use @/ for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX

Files:

  • apps/web/utils/outlook/label.ts
!{.cursor/rules/*.mdc}

📄 CodeRabbit Inference Engine (.cursor/rules/cursor-rules.mdc)

Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location

Files:

  • apps/web/utils/outlook/label.ts
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/form-handling.mdc)

**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod

Files:

  • apps/web/utils/outlook/label.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/logging.mdc)

**/*.{ts,tsx}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import Prisma in the project using import prisma from "@/utils/prisma";

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.

Files:

  • apps/web/utils/outlook/label.ts
apps/web/utils/**

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

Create utility functions in utils/ folder for reusable logic

Files:

  • apps/web/utils/outlook/label.ts
apps/web/utils/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size

Files:

  • apps/web/utils/outlook/label.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use elements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...

Files:

  • apps/web/utils/outlook/label.ts
!pages/_document.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.

Files:

  • apps/web/utils/outlook/label.ts
🧬 Code Graph Analysis (1)
apps/web/utils/outlook/label.ts (4)
apps/web/utils/outlook/client.ts (1)
  • OutlookClient (19-63)
packages/tinybird/src/publish.ts (1)
  • TinybirdEmailAction (12-12)
apps/web/app/api/outlook/webhook/logger.ts (1)
  • logger (3-3)
apps/web/utils/outlook/message.ts (1)
  • WELL_KNOWN_FOLDERS (14-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Static Code Analysis Js
  • GitHub Check: Jit Security
🔇 Additional comments (5)
apps/web/utils/outlook/label.ts (5)

4-4: LGTM! Import change aligns with refactoring.

The import of WELL_KNOWN_FOLDERS from the message module supports the new folder validation logic and replaces the previous folder name approach.


233-244: Function signature updated correctly for folderId-based operations.

The signature change from folderName to folderId with proper TypeScript typing aligns with the PR's objective to use folder IDs throughout the system.


255-271: Well-known folder validation logic is sound.

The approach of checking against WELL_KNOWN_FOLDERS first before making API calls for custom folders is efficient and reduces unnecessary network requests. The error handling appropriately logs warnings and skips operations for non-existent folders.


290-290: Consistent use of folderId in move operations.

All move operations correctly use destinationId: folderId, ensuring consistency across different code paths (primary, alternative filter, and direct message approaches).

Also applies to: 369-369, 388-388


293-293: Updated error and log messages reflect folderId terminology.

All error messages and log statements have been consistently updated to reference folderId instead of the previous folderName, maintaining clarity in debugging and monitoring.

Also applies to: 321-321, 329-335, 373-373, 402-402, 413-413

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cf14f6 and 42337a2.

📒 Files selected for processing (4)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (8 hunks)
  • apps/web/app/api/user/folders/route.ts (1 hunks)
  • apps/web/hooks/useFolders.ts (1 hunks)
  • apps/web/utils/actions/rule.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/utils/actions/rule.ts
  • apps/web/app/api/user/folders/route.ts
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
🧰 Additional context used
📓 Path-based instructions (10)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use @/ for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX

Files:

  • apps/web/hooks/useFolders.ts
apps/web/hooks/**/*.ts

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

Use SWR for efficient data fetching and caching

apps/web/hooks/**/*.ts: Use SWR for client-side data fetching and caching.
Call mutate() after successful mutations to refresh SWR data on the client.

Files:

  • apps/web/hooks/useFolders.ts
apps/web/hooks/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

Call mutate() after successful mutations to refresh data

Files:

  • apps/web/hooks/useFolders.ts
!{.cursor/rules/*.mdc}

📄 CodeRabbit Inference Engine (.cursor/rules/cursor-rules.mdc)

Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location

Files:

  • apps/web/hooks/useFolders.ts
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/form-handling.mdc)

**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod

Files:

  • apps/web/hooks/useFolders.ts
apps/web/hooks/**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/hooks.mdc)

Place custom hooks in the apps/web/hooks/ directory.

Files:

  • apps/web/hooks/useFolders.ts
apps/web/hooks/use*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/hooks.mdc)

apps/web/hooks/use*.{js,jsx,ts,tsx}: Name custom hooks with the use prefix (e.g., useAccounts.ts).
For fetching data from API endpoints in custom hooks, prefer using useSWR.
Create dedicated hooks for specific data types (e.g., useAccounts, useLabels).
Custom hooks should encapsulate reusable stateful logic, especially for data fetching or complex UI interactions.
Keep custom hooks focused on a single responsibility.

Files:

  • apps/web/hooks/useFolders.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/logging.mdc)

**/*.{ts,tsx}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import Prisma in the project using import prisma from "@/utils/prisma";

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.

Files:

  • apps/web/hooks/useFolders.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use elements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...

Files:

  • apps/web/hooks/useFolders.ts
!pages/_document.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.

Files:

  • apps/web/hooks/useFolders.ts
🧠 Learnings (10)
📚 Learning: 2025-07-18T15:04:30.467Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-07-18T15:04:30.467Z
Learning: Applies to apps/web/hooks/**/*.ts : Use SWR for efficient data fetching and caching

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-18T15:05:41.705Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/hooks.mdc:0-0
Timestamp: 2025-07-18T15:05:41.705Z
Learning: Applies to apps/web/hooks/use*.{js,jsx,ts,tsx} : Create dedicated hooks for specific data types (e.g., `useAccounts`, `useLabels`).

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-18T15:05:16.146Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-07-18T15:05:16.146Z
Learning: Applies to apps/web/hooks/**/*.ts : Use SWR for client-side data fetching and caching.

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-18T15:05:41.705Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/hooks.mdc:0-0
Timestamp: 2025-07-18T15:05:41.705Z
Learning: Applies to apps/web/hooks/use*.{js,jsx,ts,tsx} : For fetching data from API endpoints in custom hooks, prefer using `useSWR`.

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-18T17:27:58.249Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/server-actions.mdc:0-0
Timestamp: 2025-07-18T17:27:58.249Z
Learning: Applies to apps/web/utils/actions/*.ts : Use `actionClient` when both authenticated user context and a specific `emailAccountId` are needed. The `emailAccountId` must be bound when calling the action from the client.

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-18T17:27:46.389Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/security.mdc:0-0
Timestamp: 2025-07-18T17:27:46.389Z
Learning: Applies to **/api/**/route.ts : Use `withEmailAccount` middleware for API routes that operate on a specific email account (i.e., use or require `emailAccountId`).

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-18T15:04:44.818Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/data-fetching.mdc:0-0
Timestamp: 2025-07-18T15:04:44.818Z
Learning: Applies to {app,components}/**/*.{ts,tsx} : For API GET requests to server, use the `swr` package

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-19T17:50:22.078Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-07-19T17:50:22.078Z
Learning: Applies to {components,app}/**/*.tsx : For API get requests to server use the `swr` package

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-20T09:00:16.505Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/project-structure.mdc:0-0
Timestamp: 2025-07-20T09:00:16.505Z
Learning: Use `swr` for data fetching in deeply nested components

Applied to files:

  • apps/web/hooks/useFolders.ts
📚 Learning: 2025-07-18T15:05:56.644Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/index.mdc:0-0
Timestamp: 2025-07-18T15:05:56.644Z
Learning: Fetching data from the API using SWR

Applied to files:

  • apps/web/hooks/useFolders.ts
🧬 Code Graph Analysis (1)
apps/web/hooks/useFolders.ts (1)
apps/web/utils/outlook/folders.ts (1)
  • OutlookFolder (9-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Static Code Analysis Js
  • GitHub Check: Jit Security
🔇 Additional comments (1)
apps/web/hooks/useFolders.ts (1)

1-1: Path alias and type-only import are correct

Using "@/..." and import type is aligned with project conventions and tree-shaking.

@edulelis edulelis changed the title Refactor Outlook folder functionality WIP: Refactor Outlook folder functionality Aug 13, 2025
emailAccountId,
groupId: rule.groupId,
.action(
async ({ ctx: { emailAccountId, provider }, parsedInput: { id } }) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

commit hook formatting

@edulelis edulelis changed the title WIP: Refactor Outlook folder functionality Refactor Outlook folder functionality Aug 13, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🔭 Outside diff range comments (4)
apps/web/utils/rule/rule.ts (2)

365-385: folderId is not persisted in mapActionFields (Microsoft)

You’re gating folderName by provider, but folderId is not mapped at all. This will drop folderId when creating/updating rules through this path, leading to Outlook moves targeting by name only and breaking end-to-end folderId support introduced elsewhere in the PR.

Add folderId alongside folderName when provider === "microsoft".

Apply this diff:

 function mapActionFields(
   actions: CreateOrUpdateRuleSchemaWithCategories["actions"],
-  provider: string,
+  provider: string,
 ) {
   return actions.map(
     (a): Prisma.ActionCreateManyRuleInput => ({
       type: a.type,
       label: a.fields?.label,
       to: a.fields?.to,
       cc: a.fields?.cc,
       bcc: a.fields?.bcc,
       subject: a.fields?.subject,
       content: a.fields?.content,
       url: a.fields?.webhookUrl,
-      ...(provider === "microsoft" && {
-        folderName: a.fields?.folderName as string | null,
-      }),
+      ...(provider === "microsoft" && {
+        folderName: a.fields?.folderName as string | null,
+        folderId: (a.fields?.folderId as string | null) ?? null,
+      }),
       delayInMinutes: a.delayInMinutes,
     }),
   );
 }

90-99: Throw SafeError from safeCreateRule on non-duplicate failures

safeCreateRule currently logs the error and returns implicitly (undefined), which silently fails callers (makes upstream behavior unpredictable). Throw a SafeError so next-safe-action centralizes handling.

Files to change / review:

  • apps/web/utils/rule/rule.ts — catch block in safeCreateRule (currently logs then returns implicitly).
  • Call sites to verify after change (found via rg):
    • apps/web/utils/actions/ai-rule.ts (createAutomationAction and the "add new rules" loop)
    • apps/web/utils/reply-tracker/enable.ts (createToReplyRule)
    • (search command) rg -n "safeCreateRule(" -A 2

Suggested diff:

   } catch (error) {
     if (isDuplicateError(error, "name")) {
       ...
     }

     logger.error("Error creating rule", {
       emailAccountId,
       error:
         error instanceof Error
           ? { message: error.message, stack: error.stack, name: error.name }
           : error,
     });
-    // return { error: "Error creating rule." };
+    throw new SafeError("Error creating rule.");
   }

Also consider making safeUpdateRule consistent (it currently returns { error: "Error updating rule." } — prefer throwing SafeError as well) and update any callers that rely on returned error objects.

apps/web/utils/actions/rule.ts (1)

225-242: Harden updates: prevent cross-rule action updates via ID spoofing

actionsToUpdate includes any action with an id, even if it doesn’t belong to the current rule. The subsequent prisma.action.update({ where: { id: a.id } }) can mutate another rule’s action if a spoofed ID is sent. Scope updates to current rule.

Apply this diff to filter updates and scope the write:

-        const actionsToUpdate = actions.filter((a) => a.id);
+        const allowedActionIds = new Set(currentActions.map((a) => a.id));
+        const actionsToUpdate = actions.filter(
+          (a) => a.id && allowedActionIds.has(a.id),
+        );
-          ...actionsToUpdate.map((a) => {
-            return prisma.action.update({
-              where: { id: a.id },
-              data: sanitizeActionFields({
+          ...actionsToUpdate.map((a) => {
+            return prisma.action.updateMany({
+              where: { id: a.id, ruleId: id },
+              data: sanitizeActionFields({
                 type: a.type,
                 label: a.label?.value,
                 subject: a.subject?.value,
                 content: a.content?.value,
                 to: a.to?.value,
                 cc: a.cc?.value,
                 bcc: a.bcc?.value,
                 url: a.url?.value,
                 folderName: a.folderName?.value,
                 folderId: a.folderId?.value,
                 delayInMinutes: a.delayInMinutes,
               }),
             });
           }),

Note: updateMany returns a count; you’re not using the return value, so this change is non-breaking.

apps/web/utils/ai/assistant/process-user-request.ts (1)

563-586: AI-created rules drop folderId; add it to action field mapping

The tool’s create_rule path builds fields but omits folderId, so Outlook moves created via the AI assistant won’t persist the folderId. Include folderId alongside folderName.

Apply this diff:

             const rule = await createRule({
               result: {
                 name,
                 condition,
                 actions: actions.map((action) => ({
                   ...action,
                   fields: action.fields
                     ? {
                         ...action.fields,
                         label: action.fields.label ?? null,
                         to: action.fields.to ?? null,
                         cc: action.fields.cc ?? null,
                         bcc: action.fields.bcc ?? null,
                         subject: action.fields.subject ?? null,
                         content: action.fields.content ?? null,
                         webhookUrl: action.fields.webhookUrl ?? null,
                         folderName: action.fields.folderName ?? null,
+                        folderId: action.fields.folderId ?? null,
                       }
                     : null,
                 })),
               },
               emailAccountId: emailAccount.id,
               categoryIds,
               provider: emailAccount.account.provider,
             });
🧹 Nitpick comments (2)
apps/web/utils/rule/rule.ts (1)

36-45: Tighten provider typing to the Prisma Provider enum

Using string for provider invites typos and weakens type safety. Prefer Provider from @prisma/client across these signatures (and mapActionFields) to enforce valid values.

Apply representative diffs (repeat the type change for all affected signatures):

-import {
-  ActionType,
-  type SystemType,
-  type Prisma,
-  type Rule,
-} from "@prisma/client";
+import {
+  ActionType,
+  type SystemType,
+  type Prisma,
+  type Rule,
+  type Provider,
+} from "@prisma/client";
 export async function safeCreateRule({
   result,
   emailAccountId,
-  provider,
+  provider,
   categoryNames,
   systemType,
   triggerType = "ai_creation",
   shouldCreateIfDuplicate,
 }: {
   result: CreateOrUpdateRuleSchemaWithCategories;
   emailAccountId: string;
-  provider: string;
+  provider: Provider;

Do the same for:

  • safeUpdateRule
  • createRule
  • updateRule (internal)
  • updateRuleActions
  • mapActionFields

Also applies to: 44-45, 107-115, 114-115, 157-165, 164-165, 215-223, 222-223, 261-266, 365-368

apps/web/utils/actions/rule.ts (1)

399-456: Wrap after() side effects in try/catch to avoid unhandled rejections

Errors thrown inside after() run outside the outer try/catch and can become unhandled rejections (e.g., missing emailAccount). Swallow expected errors and log others.

Apply this diff:

-        after(async () => {
-          const emailAccount = await prisma.emailAccount.findUnique({
+        after(async () => {
+          try {
+            const emailAccount = await prisma.emailAccount.findUnique({
               where: { id: emailAccountId },
               select: {
                 id: true,
                 userId: true,
                 email: true,
                 about: true,
                 rulesPrompt: true,
                 user: {
                   select: {
                     aiModel: true,
                     aiProvider: true,
                     aiApiKey: true,
                   },
                 },
               },
-          });
-          if (!emailAccount) throw new SafeError("User not found");
+            });
+            if (!emailAccount) return;
 
-          if (!emailAccount.rulesPrompt) return;
+            if (!emailAccount.rulesPrompt) return;
 
-          const updatedPrompt = await generatePromptOnDeleteRule({
-            emailAccount: { ...emailAccount, account: { provider } },
-            existingPrompt: emailAccount.rulesPrompt,
-            deletedRule: rule,
-          });
+            const updatedPrompt = await generatePromptOnDeleteRule({
+              emailAccount: { ...emailAccount, account: { provider } },
+              existingPrompt: emailAccount.rulesPrompt,
+              deletedRule: rule,
+            });
 
-          await prisma.emailAccount.update({
-            where: { id: emailAccountId },
-            data: { rulesPrompt: updatedPrompt },
-          });
-        });
+            await prisma.emailAccount.update({
+              where: { id: emailAccountId },
+              data: { rulesPrompt: updatedPrompt },
+            });
+          } catch (err) {
+            // Best-effort post-delete maintenance; log and continue
+            logger.warn("Failed to update prompt after rule delete", {
+              emailAccountId,
+              error: err instanceof Error ? err.message : String(err),
+            });
+          }
+        });
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42337a2 and 1e10068.

📒 Files selected for processing (7)
  • apps/web/utils/actions/ai-rule.ts (3 hunks)
  • apps/web/utils/actions/rule.ts (5 hunks)
  • apps/web/utils/ai/assistant/chat.ts (8 hunks)
  • apps/web/utils/ai/assistant/process-user-request.ts (1 hunks)
  • apps/web/utils/ai/choose-rule/ai-choose-rule.ts (1 hunks)
  • apps/web/utils/reply-tracker/enable.ts (2 hunks)
  • apps/web/utils/rule/rule.ts (12 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/web/utils/ai/choose-rule/ai-choose-rule.ts
🧰 Additional context used
📓 Path-based instructions (12)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use @/ for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX

Files:

  • apps/web/utils/ai/assistant/process-user-request.ts
  • apps/web/utils/reply-tracker/enable.ts
  • apps/web/utils/actions/ai-rule.ts
  • apps/web/utils/ai/assistant/chat.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/rule/rule.ts
!{.cursor/rules/*.mdc}

📄 CodeRabbit Inference Engine (.cursor/rules/cursor-rules.mdc)

Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location

Files:

  • apps/web/utils/ai/assistant/process-user-request.ts
  • apps/web/utils/reply-tracker/enable.ts
  • apps/web/utils/actions/ai-rule.ts
  • apps/web/utils/ai/assistant/chat.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/rule/rule.ts
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/form-handling.mdc)

**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod

Files:

  • apps/web/utils/ai/assistant/process-user-request.ts
  • apps/web/utils/reply-tracker/enable.ts
  • apps/web/utils/actions/ai-rule.ts
  • apps/web/utils/ai/assistant/chat.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/rule/rule.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/logging.mdc)

**/*.{ts,tsx}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import Prisma in the project using import prisma from "@/utils/prisma";

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.

Files:

  • apps/web/utils/ai/assistant/process-user-request.ts
  • apps/web/utils/reply-tracker/enable.ts
  • apps/web/utils/actions/ai-rule.ts
  • apps/web/utils/ai/assistant/chat.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/rule/rule.ts
apps/web/utils/**

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

Create utility functions in utils/ folder for reusable logic

Files:

  • apps/web/utils/ai/assistant/process-user-request.ts
  • apps/web/utils/reply-tracker/enable.ts
  • apps/web/utils/actions/ai-rule.ts
  • apps/web/utils/ai/assistant/chat.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/rule/rule.ts
apps/web/utils/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size

Files:

  • apps/web/utils/ai/assistant/process-user-request.ts
  • apps/web/utils/reply-tracker/enable.ts
  • apps/web/utils/actions/ai-rule.ts
  • apps/web/utils/ai/assistant/chat.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/rule/rule.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use elements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...

Files:

  • apps/web/utils/ai/assistant/process-user-request.ts
  • apps/web/utils/reply-tracker/enable.ts
  • apps/web/utils/actions/ai-rule.ts
  • apps/web/utils/ai/assistant/chat.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/rule/rule.ts
!pages/_document.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.

Files:

  • apps/web/utils/ai/assistant/process-user-request.ts
  • apps/web/utils/reply-tracker/enable.ts
  • apps/web/utils/actions/ai-rule.ts
  • apps/web/utils/ai/assistant/chat.ts
  • apps/web/utils/actions/rule.ts
  • apps/web/utils/rule/rule.ts
apps/web/utils/{ai,llms}/**/*

📄 CodeRabbit Inference Engine (.cursor/rules/llm.mdc)

LLM-related code must be organized in the directories: apps/web/utils/ai/, apps/web/utils/llms/, and apps/web/tests/ for LLM-specific tests.

Files:

  • apps/web/utils/ai/assistant/process-user-request.ts
  • apps/web/utils/ai/assistant/chat.ts
apps/web/utils/{ai,llms}/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/llm.mdc)

apps/web/utils/{ai,llms}/**/*.ts: Keep system prompts and user prompts separate in LLM-related functions.
System prompt should define the LLM's role and task specifications.
User prompt should contain the actual data and context.
Always define a Zod schema for response validation in LLM-related functions.
Make Zod schemas as specific as possible to guide the LLM output.
Use descriptive scoped loggers for each LLM feature.
Log inputs and outputs with appropriate log levels in LLM-related functions.
Include relevant context in log messages for LLM-related code.
Implement early returns for invalid inputs in LLM-related functions.
Use proper error types and logging in LLM-related code.
Implement fallbacks for AI failures in LLM-related functions.
Add retry logic for transient failures using withRetry in LLM-related functions.
Use XML-like tags to structure data in LLM prompts.
Remove excessive whitespace and truncate long inputs in LLM prompts.
Format data consistently across similar LLM-related functions.
Use TypeScript types for all parameters and return values in LLM-related functions.
Define clear interfaces for complex input/output structures in LLM-related code.
Keep related AI functions in the same file or directory.
Extract common patterns into utility functions in LLM-related code.
Document complex AI logic with clear comments in LLM-related code.

Files:

  • apps/web/utils/ai/assistant/process-user-request.ts
  • apps/web/utils/ai/assistant/chat.ts
apps/web/utils/actions/**/*.ts

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/utils/actions/**/*.ts: Use server actions for all mutations (create/update/delete operations)
next-safe-action provides centralized error handling
Use Zod schemas for validation on both client and server
Use revalidatePath in server actions for cache invalidation

apps/web/utils/actions/**/*.ts: Use server actions (with next-safe-action) for all mutations (create/update/delete operations); do NOT use POST API routes for mutations.
Use revalidatePath in server actions to invalidate cache after mutations.

Files:

  • apps/web/utils/actions/ai-rule.ts
  • apps/web/utils/actions/rule.ts
apps/web/utils/actions/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/server-actions.mdc)

apps/web/utils/actions/*.ts: Implement all server actions using the next-safe-action library for type safety, input validation, context management, and error handling. Refer to apps/web/utils/actions/safe-action.ts for client definitions (actionClient, actionClientUser, adminActionClient).
Use actionClientUser when only authenticated user context (userId) is needed.
Use actionClient when both authenticated user context and a specific emailAccountId are needed. The emailAccountId must be bound when calling the action from the client.
Use adminActionClient for actions restricted to admin users.
Access necessary context (like userId, emailAccountId, etc.) provided by the safe action client via the ctx object in the .action() handler.
Server Actions are strictly for mutations (operations that change data, e.g., creating, updating, deleting). Do NOT use Server Actions for data fetching (GET operations). For data fetching, use dedicated GET API Routes combined with SWR Hooks.
Use SafeError for expected/handled errors within actions if needed. next-safe-action provides centralized error handling.
Use the .metadata({ name: "actionName" }) method to provide a meaningful name for monitoring. Sentry instrumentation is automatically applied via withServerActionInstrumentation within the safe action clients.
If an action modifies data displayed elsewhere, use revalidatePath or revalidateTag from next/cache within the action handler as needed.

Server action files must start with use server

Files:

  • apps/web/utils/actions/ai-rule.ts
  • apps/web/utils/actions/rule.ts
🧠 Learnings (1)
📚 Learning: 2025-07-18T15:05:34.899Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/gmail-api.mdc:0-0
Timestamp: 2025-07-18T15:05:34.899Z
Learning: Applies to apps/web/utils/gmail/**/*.ts : Keep provider-specific implementation details isolated in the appropriate utils subfolder (e.g., 'apps/web/utils/gmail/')

Applied to files:

  • apps/web/utils/ai/assistant/chat.ts
🧬 Code Graph Analysis (3)
apps/web/utils/reply-tracker/enable.ts (1)
apps/web/utils/error.ts (1)
  • SafeError (86-96)
apps/web/utils/actions/rule.ts (5)
apps/web/utils/error.ts (1)
  • SafeError (86-96)
apps/web/utils/rule/rule.ts (1)
  • deleteRule (278-294)
apps/web/utils/path.ts (1)
  • prefixPath (1-4)
apps/web/utils/ai/rule/generate-prompt-on-delete-rule.ts (1)
  • generatePromptOnDeleteRule (14-69)
apps/web/utils/prisma-helpers.ts (1)
  • isNotFoundError (14-19)
apps/web/utils/rule/rule.ts (1)
apps/web/utils/ai/rule/create-rule-schema.ts (1)
  • CreateOrUpdateRuleSchemaWithCategories (147-150)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test
  • GitHub Check: Static Code Analysis Js
  • GitHub Check: Jit Security
🔇 Additional comments (15)
apps/web/utils/actions/rule.ts (1)

81-107: LGTM: folderId is correctly persisted on create

The createRuleAction now threads folderId through sanitizeActionFields and createMany. This matches the PR’s folderId objective.

apps/web/utils/reply-tracker/enable.ts (1)

140-149: LGTM: provider is correctly fetched and threaded into safeCreateRule

Fetching account.provider and passing it to safeCreateRule aligns this flow with the provider-aware rule creation across the codebase.

apps/web/utils/actions/ai-rule.ts (1)

184-191: LGTM: provider context is correctly propagated to safeCreateRule/safeUpdateRule

All three call sites now pass emailAccount.account.provider. This keeps rule creation/update paths provider-aware and consistent with backend expectations.

Also applies to: 455-461, 486-493

apps/web/utils/ai/assistant/chat.ts (12)

179-187: LGTM! Provider parameter correctly added to createRuleTool signature.

The provider parameter is properly typed and threaded through to enable provider-specific functionality.


211-214: Provider-aware folderName handling implemented correctly.

The conditional logic properly restricts folderName to Microsoft provider only, with appropriate fallback to undefined for other providers.


220-220: Provider parameter correctly passed to createRule.

The provider value is properly threaded through to the rule creation logic.


273-281: Provider parameter correctly added to updateRuleConditionsTool.

The function signature is updated to accept the provider parameter, maintaining consistency with other tool functions.


359-367: Provider parameter correctly added to updateRuleActionsTool.

The function signature properly includes the provider parameter for provider-aware action handling.


445-445: Provider-aware folderName filtering in original actions.

The logic correctly includes folderName in the original actions only when the provider is Microsoft, maintaining consistency with the provider-specific behavior.


461-464: Provider-aware folderName handling in action updates.

The conditional logic properly restricts folderName assignment to Microsoft provider only during action updates.


468-468: Provider parameter correctly passed to updateRuleActions.

The provider value is properly threaded through to the rule action update logic.


484-492: Provider parameter added to updateLearnedPatternsTool signature.

While the provider parameter is added for consistency with other tools, it's not currently used within this function's logic.


590-598: Provider parameter added to updateAboutTool signature.

The provider parameter is included for consistency with other tool functions, though it's not used in the about update logic.


628-636: Provider parameter added to addToKnowledgeBaseTool signature.

The provider parameter maintains consistency across all tool functions, even though it's not used in the knowledge base logic.


943-947: Provider correctly extracted and passed to tool options.

The provider value is properly extracted from user.account.provider and included in toolOptions for all tool functions.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/web/utils/outlook/label.ts (1)

246-253: Add validation for empty or malformed folderId.

The current validation only checks if folderId is falsy. However, it should also validate for empty strings, whitespace-only strings, or potentially malformed values before making API calls.

Apply this diff to add proper validation:

-  if (!folderId) {
-    logger.warn("No folderId provided, skipping archive operation", {
+  // Validate folderId
+  if (!folderId || typeof folderId !== 'string' || !folderId.trim()) {
+    logger.warn("Invalid or empty folderId provided, skipping archive operation", {
+      folderId,
       threadId,
       ownerEmail,
       actionSource,
     });
     return;
   }
🧹 Nitpick comments (5)
apps/web/utils/outlook/label.ts (5)

238-245: Consider using a more descriptive parameter name.

The parameter name folderId suggests it should be a unique identifier (like a GUID), but it can also accept well-known folder names like "archive". Consider renaming to folderIdOrName or folderTarget to better reflect its dual nature.


255-271: Improve error handling for folder existence check.

The current implementation only logs a warning when a custom folder is not found. Consider distinguishing between different error types (404 vs other errors) to provide more specific feedback.

Apply this diff to improve error handling:

   // Check if the destination folder exists (only for custom folders, well-known names can be trusted and used directly)
   const wellKnownFolders = Object.keys(WELL_KNOWN_FOLDERS);
   if (!wellKnownFolders.includes(folderId.toLowerCase())) {
     try {
       await client.getClient().api(`/me/mailFolders/${folderId}`).get();
     } catch (error) {
+      const isNotFound = error instanceof Error && 
+        (error.message?.includes('404') || 
+         error.message?.includes('not found') ||
+         error.message?.includes('ResourceNotFound'));
+      
       logger.warn(
-        "Custom destination folder not found, skipping archive operation",
+        isNotFound 
+          ? "Custom destination folder not found, skipping archive operation"
+          : "Failed to verify custom destination folder, skipping archive operation",
         {
           folderId,
           threadId,
-          error,
+          error: error instanceof Error ? error.message : error,
+          errorType: isNotFound ? 'not_found' : 'verification_failed',
         },
       );
       return;
     }
   }

322-327: Inconsistent error message format.

The error message on line 322 uses template literal while other similar messages use object properties for logging context.

Apply this diff for consistency:

-      logger.error(`Failed to move thread to folder ${folderId}`, {
+      logger.error("Failed to move thread to folder", {
+        folderId,
         threadId,
         error,
       });

330-337: Remove redundant template literal in error message.

Similar to the previous comment, the error message uses a template literal unnecessarily.

Apply this diff:

-      logger.error(
-        `Failed to publish action to move thread to folder ${folderId}`,
-        {
-          threadId,
-          error: publishResult.reason,
-        },
-      );
+      logger.error("Failed to publish action to move thread to folder", {
+        folderId,
+        threadId,
+        error: publishResult.reason,
+      });

346-420: Consider extracting the fallback logic into a separate function.

The error handling block contains substantial logic for alternative approaches to move messages. This makes the function harder to read and test. Consider extracting this into a separate helper function.

Add a new helper function before archiveThread:

async function moveThreadMessagesFallback({
  client,
  threadId,
  folderId,
  ownerEmail,
  actionSource,
}: {
  client: OutlookClient;
  threadId: string;
  folderId: string;
  ownerEmail: string;
  actionSource: TinybirdEmailAction["actionSource"];
}) {
  // Try to get messages by conversationId using a different endpoint
  const messages = await client
    .getClient()
    .api("/me/messages")
    .select("id")
    .get();

  // Filter messages by conversationId manually
  const threadMessages = messages.value.filter(
    (message: { conversationId: string }) =>
      message.conversationId === threadId,
  );

  if (threadMessages.length > 0) {
    // Move each message in the thread to the destination folder
    const movePromises = threadMessages.map(
      async (message: { id: string }) => {
        try {
          return await client
            .getClient()
            .api(`/me/messages/${message.id}/move`)
            .post({
              destinationId: folderId,
            });
        } catch (moveError) {
          logger.warn("Failed to move message to folder", {
            folderId,
            messageId: message.id,
            threadId,
            error:
              moveError instanceof Error ? moveError.message : moveError,
          });
          return null;
        }
      },
    );

    await Promise.allSettled(movePromises);
  } else {
    // If no messages found, try treating threadId as a messageId
    await client.getClient().api(`/me/messages/${threadId}/move`).post({
      destinationId: folderId,
    });
  }

  // Publish the archive action
  try {
    await publishArchive({
      ownerEmail,
      threadId,
      actionSource,
      timestamp: Date.now(),
    });
  } catch (publishError) {
    logger.error("Failed to publish action to move thread to folder", {
      folderId,
      email: ownerEmail,
      threadId,
      error: publishError,
    });
  }
}

Then simplify the error handling in archiveThread:

   } catch (error) {
     // If the filter fails, try a different approach
     logger.warn("Filter failed, trying alternative approach", {
       threadId,
       error,
     });

     try {
-      // Try to get messages by conversationId using a different endpoint
-      const messages = await client
-        .getClient()
-        .api("/me/messages")
-        .select("id")
-        .get();
-
-      // ... rest of the fallback logic ...
+      await moveThreadMessagesFallback({
+        client,
+        threadId,
+        folderId,
+        ownerEmail,
+        actionSource,
+      });

       return { status: 200 };
     } catch (directError) {
       logger.error("Failed to move thread to folder", {
         folderId,
         threadId,
         error: directError,
       });
       throw directError;
     }
   }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e10068 and 82106c3.

📒 Files selected for processing (1)
  • apps/web/utils/outlook/label.ts (7 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use @/ for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX

Files:

  • apps/web/utils/outlook/label.ts
!{.cursor/rules/*.mdc}

📄 CodeRabbit Inference Engine (.cursor/rules/cursor-rules.mdc)

Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location

Files:

  • apps/web/utils/outlook/label.ts
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/form-handling.mdc)

**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod

Files:

  • apps/web/utils/outlook/label.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/logging.mdc)

**/*.{ts,tsx}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import Prisma in the project using import prisma from "@/utils/prisma";

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.

Files:

  • apps/web/utils/outlook/label.ts
apps/web/utils/**

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

Create utility functions in utils/ folder for reusable logic

Files:

  • apps/web/utils/outlook/label.ts
apps/web/utils/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/project-structure.mdc)

apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size

Files:

  • apps/web/utils/outlook/label.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use elements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...

Files:

  • apps/web/utils/outlook/label.ts
!pages/_document.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.

Files:

  • apps/web/utils/outlook/label.ts
🧬 Code Graph Analysis (1)
apps/web/utils/outlook/label.ts (3)
apps/web/utils/outlook/client.ts (1)
  • OutlookClient (19-63)
packages/tinybird/src/publish.ts (1)
  • TinybirdEmailAction (12-12)
apps/web/utils/outlook/message.ts (1)
  • WELL_KNOWN_FOLDERS (14-21)

emailAccountId: string,
addDigest: boolean,
) {
const emailAccount = await prisma.emailAccount.findUnique({
Copy link
Owner

Choose a reason for hiding this comment

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

  1. You can just pass in provider. I'd really think before making db calls unnecessarily like this. We're passing provider around all over the app. There's a very high chance the function calling this already has that info.

  2. not actually sure how critical it is to pass provider down. i guess it's just to check we don't create a folder rule for gmail users?

url: a.fields?.webhookUrl,
folderName: a.fields?.folderName,
...(provider === "microsoft" && {
folderName: a.fields?.folderName as string | null,
Copy link
Owner

Choose a reason for hiding this comment

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

we avoid as here?

or folderName || null might fix it?

error: publishResult.reason,
});
logger.error(
`Failed to publish action to move thread to folder ${folderId}`,
Copy link
Owner

Choose a reason for hiding this comment

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

i've made this note before. if you're adding logs, then don't use variables in it. harder to analyse logs this way

return { status: 404, message: "Thread not found" };
}
logger.error(`Failed to move thread to ${folderName}`, {
logger.error(`Failed to move thread to folder ${folderId}`, {
Copy link
Owner

Choose a reason for hiding this comment

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

log with var

for (const folder of folderList) {
if (!folder.childFolders || folder.childFolders.length === 0) {
try {
const childFolders = await getFolders(
Copy link
Owner

Choose a reason for hiding this comment

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

why pass path in? wouldn't it be clearer to do getChildFolders?

return response.value;
};

const folders = await getFolders("/me/mailFolders");
Copy link
Owner

Choose a reason for hiding this comment

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

no need to pass in path. see comment below

subject: action.subject,
webhookUrl: action.url,
folderName: action.folderName,
folderName: provider === "microsoft" ? action.folderName : undefined,
Copy link
Owner

Choose a reason for hiding this comment

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

might make sense to have isMicrosoft helper instead of magic string each time.

@elie222 elie222 merged commit 67349a3 into elie222:main Aug 13, 2025
9 of 10 checks passed
@edulelis edulelis deleted the refactor-move-to-folder-action branch August 27, 2025 20:32
@coderabbitai coderabbitai bot mentioned this pull request Dec 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments