Skip to content

Conversation

@Brendonovich
Copy link
Contributor

@Brendonovich Brendonovich commented Aug 15, 2025

Summary by CodeRabbit

  • New Features

    • Backend folder-creation API added; folders now support color, optional space, and optional parent.
  • Refactor

    • Folder-creation UI switched to RPC/mutation flows—dialogs show pending state, toast feedback, and auto-refresh on success.
  • Chores

    • User session now includes active organization context.
  • Revert

    • Removed legacy shared-spaces action.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 15, 2025

Walkthrough

This PR removes legacy server-actions for caps and folders, adds a Folder.create service and FolderCreate RPC, updates frontend dialogs to use RPC-based mutations with Option-wrapped fields, and extends the authenticated user context to include activeOrgId across backend and domain layers.

Changes

Cohort / File(s) Summary
Removed legacy actions
apps/web/actions/caps/get-shared-spaces.ts, apps/web/actions/folders/createFolder.ts
Deleted server actions getSharedSpacesForCap and createFolder, removing their exported APIs and associated data-fetching/creation logic.
Frontend: dialog -> RPC/mutation
apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx, apps/web/app/(org)/dashboard/folder/[id]/components/SubfolderDialog.tsx
Replaced direct server actions with useEffectMutation + withRpc(FolderCreate); wrap optional fields with Option; use mutation isPending for loading; reset form, close dialog, router.refresh() and show toasts on success/error; SubfolderDialog prop parentFolderId now Folder.FolderId.
Backend auth propagation
packages/web-backend/src/Auth.ts, packages/web-backend/src/Rpcs.ts, packages/web-domain/src/Authentication.ts
CurrentUser context payload extended to include activeOrgId; auth middleware now maps authenticated users to { id, email, activeOrgId } and propagates that through RPC middleware.
Backend RPC wiring
packages/web-backend/src/Folders/FoldersRpcs.ts
Added FolderCreate RPC entry wired to folders.create(...), mapping DatabaseError to InternalError consistent with existing RPC error patterns.
Folders service: create
packages/web-backend/src/Folders/index.ts
Added Folders.create operation: validates optional parent exists in current org, generates id (nanoId), inserts folder (converting Option to null where needed), and returns Folder domain model.
Domain models & RPC schema
packages/web-domain/src/Folder.ts
Introduced FolderColor, Folder class model (id, name, color, organizationId, createdById, optional spaceId/parentId) and FolderCreate RPC schema (auth-protected) accepting Option-wrapped spaceId/parentId.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Dialog (New/Subfolder)
  participant RPC as withRpc(FolderCreate)
  participant Auth as RpcAuthMiddleware
  participant BE as FolderRpcsLive
  participant SVC as Folders.create
  participant DB as Database

  UI->>RPC: mutate({ name, color, spaceId?: Option, parentId?: Option })
  RPC->>Auth: authenticate request
  Auth->>BE: invoke FolderCreate (with currentUser.activeOrgId)
  BE->>SVC: create(data)
  SVC->>DB: validate parent in org, insert folder
  DB-->>SVC: created row
  SVC-->>BE: Folder
  BE-->>RPC: Folder
  RPC-->>UI: success
  UI->>UI: reset form, close, router.refresh(), toast
Loading
sequenceDiagram
  participant Req as HTTP Request
  participant AuthMW as HttpAuthMiddlewareLive
  participant Ctx as CurrentUser.context

  Req->>AuthMW: incoming request (with user)
  AuthMW->>Ctx: set { id, email, activeOrgId }
  Ctx-->>AuthMW: context available downstream
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A nibble of code, a hop to create,
Old actions cleared, new RPCs await.
Colors picked, parents checked just right,
activeOrgId guiding each small byte.
Refresh, a toast — the rabbit hops with delight! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch folder-create-effect

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.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

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.

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: 5

🧹 Nitpick comments (4)
apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx (1)

150-153: Optional UX: notify when color is not selected

Currently, clicking “Create” with no color silently no-ops. Consider warning the user (e.g., toast) to clarify why nothing happened.

   onClick={() => {
-    if (selectedColor === null) return;
+    if (selectedColor === null) {
+      toast.error("Please select a folder color");
+      return;
+    }
     createFolder.mutate({ name: folderName, color: selectedColor });
   }}
packages/web-backend/src/Folders/index.ts (1)

65-70: Optional: consider authorization policy for creation

You already apply policy checks on delete. If creation requires specific permissions (e.g., per-space or org role), consider enforcing policy.canCreate(...) similarly.

apps/web/app/(org)/dashboard/folder/[id]/components/SubfolderDialog.tsx (1)

79-89: Trim folder name before sending to RPC to avoid accidental leading/trailing spaces

Small UX/validation improvement: you already disable the button when the trimmed name is empty; trim the value before sending it to the backend as well.

   mutationFn: (data: { name: string; color: FolderColor }) =>
     withRpc((r) =>
       r.FolderCreate({
-        name: data.name,
+        name: data.name.trim(),
         color: data.color,
         spaceId: Option.fromNullable(activeSpace?.id),
         parentId: Option.some(parentFolderId),
       }),
     ),
   onClick={() => {
     if (selectedColor === null) return;
     createSubfolder.mutate({
-      name: folderName,
+      name: folderName.trim(),
       color: selectedColor,
     });
   }}

Also applies to: 165-167

packages/web-domain/src/Folder.ts (1)

33-43: Verify PolicyDeniedError for FolderCreate RPC

I wasn’t able to find any existing policy‐denial checks around folder creation in the codebase (no throws of PolicyDeniedError, and the Delete RPC already includes it). Please double-check whether the Folders service enforces authorization on create—if so, callers will need to distinguish a policy denial from NotFoundError/InternalError.

• Manually inspect the backend Folders service’s create handler for any PolicyDeniedError throws or authorization logic.
• If policy checks exist (or are planned), update the RPC definition in packages/web-domain/src/Folder.ts:

   Rpc.make("FolderCreate", {
     payload: Schema.Struct({
       name: Schema.String,
       color: FolderColor,
       spaceId: Schema.OptionFromUndefinedOr(Schema.String),
       parentId: Schema.OptionFromUndefinedOr(FolderId),
     }),
     success: Folder,
-    error: Schema.Union(NotFoundError, InternalError),
+    error: Schema.Union(NotFoundError, InternalError, PolicyDeniedError),
   }).middleware(RpcAuthMiddleware),
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 801757a and 74c2ff7.

📒 Files selected for processing (9)
  • apps/web/actions/caps/get-shared-spaces.ts (0 hunks)
  • apps/web/actions/folders/createFolder.ts (0 hunks)
  • apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx (4 hunks)
  • apps/web/app/(org)/dashboard/folder/[id]/components/SubfolderDialog.tsx (4 hunks)
  • packages/web-backend/src/Auth.ts (2 hunks)
  • packages/web-backend/src/Folders/FoldersRpcs.ts (1 hunks)
  • packages/web-backend/src/Folders/index.ts (3 hunks)
  • packages/web-domain/src/Authentication.ts (1 hunks)
  • packages/web-domain/src/Folder.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • apps/web/actions/caps/get-shared-spaces.ts
  • apps/web/actions/folders/createFolder.ts
🧰 Additional context used
🧬 Code Graph Analysis (5)
packages/web-backend/src/Folders/FoldersRpcs.ts (2)
packages/database/schema.ts (1)
  • folders (205-229)
packages/web-domain/src/Errors.ts (1)
  • InternalError (3-6)
packages/web-backend/src/Folders/index.ts (4)
packages/web-domain/src/Folder.ts (5)
  • Folder (18-26)
  • FolderColor (10-10)
  • FolderColor (11-11)
  • FolderId (7-7)
  • FolderId (8-8)
packages/web-domain/src/Authentication.ts (1)
  • CurrentUser (7-10)
packages/database/index.ts (1)
  • db (28-33)
packages/database/helpers.ts (1)
  • nanoId (6-9)
apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx (3)
apps/web/lib/EffectRuntime.ts (1)
  • useEffectMutation (19-19)
packages/web-domain/src/Folder.ts (3)
  • Folder (18-26)
  • FolderColor (10-10)
  • FolderColor (11-11)
apps/web/lib/Rpcs.ts (1)
  • withRpc (16-17)
apps/web/app/(org)/dashboard/folder/[id]/components/SubfolderDialog.tsx (3)
packages/web-domain/src/Folder.ts (5)
  • Folder (18-26)
  • FolderId (7-7)
  • FolderId (8-8)
  • FolderColor (10-10)
  • FolderColor (11-11)
apps/web/lib/EffectRuntime.ts (1)
  • useEffectMutation (19-19)
apps/web/lib/Rpcs.ts (1)
  • withRpc (16-17)
packages/web-domain/src/Folder.ts (5)
packages/web-domain/src/Video.ts (1)
  • NotFoundError (75-78)
apps/web/lib/Rpcs.ts (1)
  • Rpc (11-14)
packages/web-domain/src/Errors.ts (1)
  • InternalError (3-6)
packages/web-domain/src/Policy.ts (1)
  • PolicyDeniedError (18-21)
packages/web-domain/src/Authentication.ts (1)
  • RpcAuthMiddleware (28-34)
⏰ 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). (4)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (10)
packages/web-domain/src/Authentication.ts (1)

7-10: Confirm activeOrgId is guaranteed non-null in all auth contexts

Expanding CurrentUser to include activeOrgId: string looks aligned with the rest of the PR. Please verify that activeOrganizationId in the DB/session is always present for authenticated users; if it can be null/undefined (e.g., users not yet joined to any org), callers will receive an invalid CurrentUser context. If nullability is possible, consider modeling this as Option<string> or guarding at the middleware level.

packages/web-backend/src/Folders/FoldersRpcs.ts (1)

20-25: LGTM: FolderCreate RPC mirrors existing error-handling pattern

The implementation correctly delegates to folders.create and maps DatabaseError to InternalError({ type: "database" }), consistent with FolderDelete.

packages/web-backend/src/Auth.ts (2)

48-52: Guard against nullable activeOrganizationId before populating CurrentUser

If user.activeOrganizationId can be null/undefined, this will violate the CurrentUser contract (activeOrgId: string) and may leak an invalid context into request handling. Consider:

  • Rejecting with Unauthorized if activeOrganizationId is missing, or
  • Modeling activeOrgId as optional/Option at the domain level.

Please confirm DB/session guarantees here.


74-78: Mirror the same nullability guard in provideOptionalAuth

The optional auth path should enforce the same invariant for activeOrgId. Today it passes through potentially nullish values into CurrentUser.context.

packages/web-backend/src/Folders/index.ts (1)

91-92: Verify Folder.FolderId.make exists (or switch to a supported constructor)

If Folder.FolderId is a branded Schema.String, a .make helper may not exist. Consider switching to a supported constructor, e.g., a brand assertion or a schema decode.

Example alternatives:

  • Using a brand assertion:
    id: nanoId() as Folder.FolderId
  • Or using schema decode (if available in your helpers):
    id: Schema.decodeSync(Folder.FolderId)(nanoId())
apps/web/app/(org)/dashboard/folder/[id]/components/SubfolderDialog.tsx (2)

70-71: LGTM: Router refresh after success

Using useRouter().refresh() after a successful mutation is consistent with Next.js app router patterns to revalidate server components.


170-176: LGTM: Loading and disabled states are wired correctly

Spinner and disabled conditions correctly depend on isPending and basic input validation.

packages/web-domain/src/Folder.ts (3)

10-12: LGTM: Public FolderColor type looks good

Explicit literal union for folder colors is clear and enforces valid values across the stack.


13-17: LGTM: Specific NotFoundError

Dedicated FolderNotFoundError aligns with existing patterns (e.g., VideoNotFoundError) and improves error specificity.


18-26: LGTM: Folder model with Option fields

Using OptionFromNullOr for spaceId and parentId is appropriate for persistence layers that store null while exposing Option to callers.

DialogTitle,
Input,
} from "@cap/ui";
import type { Folder } from "@cap/web-domain";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix type import: Folder.FolderColor is not a named type import

Using Folder.FolderColor with import type { Folder } is likely a type error. Import the color type directly and use it in the mutation payload.

Apply this diff:

-import type { Folder } from "@cap/web-domain";
+import type { FolderColor } from "@cap/web-domain";
📝 Committable suggestion

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

Suggested change
import type { Folder } from "@cap/web-domain";
import type { FolderColor } from "@cap/web-domain";
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx around line
12, the code imports only "Folder" and then uses "Folder.FolderColor" which is
not a valid named type import; update the import to also pull the color type
directly (e.g. import type { Folder, FolderColor } from "@cap/web-domain") and
change any usage of Folder.FolderColor in the mutation payload and related type
annotations to use FolderColor instead.

Comment on lines +68 to +76
mutationFn: (data: { name: string; color: Folder.FolderColor }) =>
withRpc((r) =>
r.FolderCreate({
name: data.name,
color: data.color,
spaceId: Option.fromNullable(spaceId),
parentId: Option.none(),
}),
),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Align the mutation payload type with the corrected import

Update the payload type to reference FolderColor (imported directly) instead of Folder.FolderColor.

Apply this diff:

-  mutationFn: (data: { name: string; color: Folder.FolderColor }) =>
+  mutationFn: (data: { name: string; color: FolderColor }) =>
📝 Committable suggestion

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

Suggested change
mutationFn: (data: { name: string; color: Folder.FolderColor }) =>
withRpc((r) =>
r.FolderCreate({
name: data.name,
color: data.color,
spaceId: Option.fromNullable(spaceId),
parentId: Option.none(),
}),
),
mutationFn: (data: { name: string; color: FolderColor }) =>
withRpc((r) =>
r.FolderCreate({
name: data.name,
color: data.color,
spaceId: Option.fromNullable(spaceId),
parentId: Option.none(),
}),
),
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx around lines
68 to 76, the mutation payload type currently uses Folder.FolderColor but the
module import provides FolderColor directly; update the type annotation to use
FolderColor instead of Folder.FolderColor (i.e., change (data: { name: string;
color: Folder.FolderColor }) to (data: { name: string; color: FolderColor }) ),
and ensure the FolderColor import is present at the top of the file.

Comment on lines 25 to 27
import { Folder } from "@cap/web-domain";
import { Option } from "effect";
import { withRpc } from "@/lib/Rpcs";
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix incorrect type references: use top-level FolderId/FolderColor instead of Folder.FolderId/Folder.FolderColor

The domain exports FolderId and FolderColor as top-level types (see packages/web-domain/src/Folder.ts). Referencing them via Folder.* will fail. Prefer type-only imports to avoid bundling.

-import { Folder } from "@cap/web-domain";
+import type { FolderId, FolderColor } from "@cap/web-domain";
-interface Props {
+interface Props {
   open: boolean;
   onOpenChange: (open: boolean) => void;
-  parentFolderId: Folder.FolderId;
+  parentFolderId: FolderId;
 }
-  mutationFn: (data: { name: string; color: Folder.FolderColor }) =>
+  mutationFn: (data: { name: string; color: FolderColor }) =>

Also applies to: 33-33, 80-80

🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/folder/[id]/components/SubfolderDialog.tsx
around lines 25-27 (and also affecting lines 33 and 80), the code incorrectly
references Folder.FolderId and Folder.FolderColor; import and use the top-level
types instead by adding type-only imports like: import type { FolderId,
FolderColor } from "@cap/web-domain"; then replace any Folder.FolderId and
Folder.FolderColor usages with FolderId and FolderColor respectively; use type
imports to avoid bundling runtime code.

Comment on lines +73 to +89
if (Option.isSome(data.parentId)) {
const parentId = data.parentId.value;
const [parentFolder] = yield* db.execute((db) =>
db
.select()
.from(Db.folders)
.where(
Dz.and(
Dz.eq(Db.folders.id, parentId),
Dz.eq(Db.folders.organizationId, user.activeOrgId),
),
),
);

if (!parentFolder) return yield* new Folder.NotFoundError();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enforce parent/space consistency to preserve invariants

If a parentId is provided, the new folder must inherit the parent’s spaceId. Allowing a mismatch (or keeping the caller-provided spaceId) can create invalid trees (e.g., a child in root with a parent inside a space). Override the incoming spaceId with the parent’s spaceId when parentId is Some.

Apply this diff:

-        const user = yield* CurrentUser;
+        const user = yield* CurrentUser;
+        // Use parent’s spaceId if creating a subfolder
+        let effectiveSpaceId = data.spaceId;
 
         if (Option.isSome(data.parentId)) {
           const parentId = data.parentId.value;
           const [parentFolder] = yield* db.execute((db) =>
             db
               .select()
               .from(Db.folders)
               .where(
                 Dz.and(
                   Dz.eq(Db.folders.id, parentId),
                   Dz.eq(Db.folders.organizationId, user.activeOrgId),
                 ),
               ),
           );
 
           if (!parentFolder) return yield* new Folder.NotFoundError();
+          // Inherit space from parent to avoid space/root mismatches
+          effectiveSpaceId = Option.fromNullable(parentFolder.spaceId);
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/web-backend/src/Folders/index.ts around lines 73 to 89, when a
parentId is provided the code checks the parent exists but does not enforce that
the new folder inherits the parent's spaceId; update the logic so after fetching
parentFolder (and verifying it exists) you override the incoming data.spaceId
with parentFolder.spaceId (i.e., set data.spaceId = parentFolder.spaceId or
construct the new folder payload using parentFolder.spaceId) to ensure child
folders always inherit their parent’s space and preserve tree invariants.

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx (1)

106-139: Do not call Hooks inside loops; replace per-item useRef with a callback ref

useRef is a Hook and cannot be called inside map. This violates Rules of Hooks and can break rendering.

Apply this diff:

-              const folderRef = useRef<any>(null);
-              folderRefs.current[option.value] = folderRef;
+              // Store element refs in a map via callback refs (no hooks in loops)

...
-                  onMouseEnter={() => {
-                    const folderRef = folderRefs.current[option.value]?.current;
-                    if (!folderRef) return;
-                    folderRef.stop();
-                    folderRef.play("folder-open");
-                  }}
+                  onMouseEnter={() => {
+                    const refEl = folderRefs.current[option.value];
+                    if (!refEl) return;
+                    refEl.stop();
+                    refEl.play("folder-open");
+                  }}
                   onMouseLeave={() => {
-                    const folderRef = folderRefs.current[option.value]?.current;
-                    if (!folderRef) return;
-                    folderRef.stop();
-                    folderRef.play("folder-close");
+                    const refEl = folderRefs.current[option.value];
+                    if (!refEl) return;
+                    refEl.stop();
+                    refEl.play("folder-close");
                   }}
                 >
-                  {React.cloneElement(option.component, { ref: folderRef })}
+                  {React.cloneElement(option.component, {
+                    ref: (el: any) => {
+                      folderRefs.current[option.value] = el;
+                    },
+                  })}

If the folder components don’t forward refs to the underlying animation API, we can swap to an explicit onMouseEnter/Leave prop or a small wrapper that exposes stop/play.

♻️ Duplicate comments (3)
packages/web-backend/src/Folders/index.ts (2)

72-87: Inherit parent’s spaceId when creating subfolders to avoid invalid trees

If parentId is provided, the new folder must inherit the parent’s spaceId. Otherwise, callers can create a child in a different “root” (space vs root), violating invariants and breaking delete/move logic.

Apply this diff to compute and enforce an effectiveSpaceId derived from the parent:

-        const user = yield* CurrentUser;
+        const user = yield* CurrentUser;
+        // Use parent’s spaceId if creating a subfolder
+        let effectiveSpaceId = data.spaceId;

         if (Option.isSome(data.parentId)) {
           const parentId = data.parentId.value;
           const [parentFolder] = yield* db.execute((db) =>
             db
               .select()
               .from(Db.folders)
               .where(
                 Dz.and(
                   Dz.eq(Db.folders.id, parentId),
                   Dz.eq(Db.folders.organizationId, user.activeOrgId),
                 ),
               ),
           );

           if (!parentFolder) return yield* new Folder.NotFoundError();
+          // Inherit space from parent to avoid space/root mismatches
+          effectiveSpaceId = Option.fromNullable(parentFolder.spaceId);
         }

89-97: Use the computed effectiveSpaceId for persistence and return value

After deriving effectiveSpaceId above, ensure both the stored row and returned DTO use it instead of the caller-provided spaceId.

Apply this diff:

         const folder = {
           id: Folder.FolderId.make(nanoId()),
           name: data.name,
           color: data.color,
           organizationId: user.activeOrgId,
           createdById: user.id,
-          spaceId: data.spaceId,
+          spaceId: effectiveSpaceId,
           parentId: data.parentId,
         };

         yield* db.execute((db) =>
           db.insert(Db.folders).values({
             ...folder,
-            spaceId: Option.getOrNull(folder.spaceId),
+            spaceId: Option.getOrNull(effectiveSpaceId),
             parentId: Option.getOrNull(folder.parentId),
           }),
         );

Also applies to: 99-105

apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx (1)

10-10: Fix type import: use FolderColor instead of Folder.FolderColor

Folder.FolderColor isn’t a valid type when importing type-only Folder. Import FolderColor directly.

Apply this diff:

-import type { Folder } from "@cap/web-domain";
+import type { FolderColor } from "@cap/web-domain";
🧹 Nitpick comments (2)
packages/web-backend/src/Folders/index.ts (1)

89-97: Server-side normalize/validate name

UI ensures a non-empty trimmed name, but the backend accepts any string. Consider trimming and enforcing non-empty (and perhaps a reasonable max length) server-side.

Example:

-        const folder = {
+        const name = data.name.trim();
+        if (name.length === 0) return yield* new Folder.NotFoundError(); // or a dedicated validation error
+        const folder = {
           id: Folder.FolderId.make(nanoId()),
-          name: data.name,
+          name,

If you prefer a typed validation, we can strengthen the RPC payload schema (minLength) instead.

apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx (1)

56-58: Optional: type selectedColor as FolderColor | null

This avoids inference through FolderOptions and keeps the type aligned with the domain.

Apply this diff:

-  const [selectedColor, setSelectedColor] = useState<
-    (typeof FolderOptions)[number]["value"] | null
-  >(null);
+  const [selectedColor, setSelectedColor] = useState<FolderColor | null>(null);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 74c2ff7 and 3e46f43.

📒 Files selected for processing (5)
  • apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx (3 hunks)
  • apps/web/app/(org)/dashboard/folder/[id]/components/SubfolderDialog.tsx (5 hunks)
  • packages/web-backend/src/Folders/FoldersRpcs.ts (1 hunks)
  • packages/web-backend/src/Folders/index.ts (2 hunks)
  • packages/web-domain/src/Folder.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/web-backend/src/Folders/FoldersRpcs.ts
  • apps/web/app/(org)/dashboard/folder/[id]/components/SubfolderDialog.tsx
🧰 Additional context used
🧬 Code Graph Analysis (3)
packages/web-domain/src/Folder.ts (3)
packages/web-domain/src/Errors.ts (1)
  • InternalError (3-6)
packages/web-domain/src/Policy.ts (1)
  • PolicyDeniedError (18-21)
packages/web-domain/src/Authentication.ts (1)
  • RpcAuthMiddleware (28-34)
packages/web-backend/src/Folders/index.ts (3)
packages/web-domain/src/Folder.ts (5)
  • Folder (18-26)
  • FolderColor (10-10)
  • FolderColor (11-11)
  • FolderId (7-7)
  • FolderId (8-8)
packages/web-domain/src/Authentication.ts (1)
  • CurrentUser (7-10)
packages/database/helpers.ts (1)
  • nanoId (6-9)
apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx (3)
apps/web/lib/EffectRuntime.ts (1)
  • useEffectMutation (19-19)
packages/web-domain/src/Folder.ts (3)
  • Folder (18-26)
  • FolderColor (10-10)
  • FolderColor (11-11)
apps/web/lib/Rpcs.ts (1)
  • withRpc (16-17)
⏰ 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). (4)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (5)
packages/web-backend/src/Folders/index.ts (1)

64-87: Consider enforcing policy on create (and align RPC error union)

You’re initializing policy but not using it in create. If creation should be restricted (e.g., space-level constraints or org roles), add a policy guard and include PolicyDeniedError in FolderCreate’s error union.

Do you intend folder creation to be unrestricted for any authenticated user in the active org? If not, I can wire a policy check and update the RPC contract.

packages/web-domain/src/Folder.ts (3)

10-12: LGTM: FolderColor surface looks correct

String-literal union matches UI options and backend usage.


18-26: LGTM: Folder model shape aligns with DB and service layer

OptionFromNullOr matches nullable DB columns; fields align with backend insert/return shape.


33-43: Confirm create policy stance; include PolicyDeniedError if applicable

FolderDelete includes PolicyDeniedError, but FolderCreate doesn’t. If you add creation policy checks in the backend (recommended), include PolicyDeniedError here as well to keep contracts consistent.

Apply this diff if you decide to enforce policy on create:

-  error: Schema.Union(NotFoundError, InternalError),
+  error: Schema.Union(NotFoundError, InternalError, PolicyDeniedError),

Also consider tightening name validation at the schema level (e.g., min length).

apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx (1)

150-164: Guard + types are fine; minor UX polish

The pre-check for selectedColor is correct. With the trim above, users won’t accidentally create “spacey” names. No further changes needed here.

Comment on lines +67 to +77
const createFolder = useEffectMutation({
mutationFn: (data: { name: string; color: Folder.FolderColor }) =>
withRpc((r) =>
r.FolderCreate({
name: data.name,
color: data.color,
spaceId: Option.fromNullable(spaceId),
parentId: Option.none(),
}),
),
onSuccess: () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Align mutation payload type with FolderColor and trim the name

Use FolderColor for the color type and send a trimmed name to avoid leading/trailing spaces.

Apply this diff:

-  const createFolder = useEffectMutation({
-    mutationFn: (data: { name: string; color: Folder.FolderColor }) =>
+  const createFolder = useEffectMutation({
+    mutationFn: (data: { name: string; color: FolderColor }) =>
       withRpc((r) =>
         r.FolderCreate({
-          name: data.name,
+          name: data.name.trim(),
           color: data.color,
           spaceId: Option.fromNullable(spaceId),
           parentId: Option.none(),
         }),
       ),
📝 Committable suggestion

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

Suggested change
const createFolder = useEffectMutation({
mutationFn: (data: { name: string; color: Folder.FolderColor }) =>
withRpc((r) =>
r.FolderCreate({
name: data.name,
color: data.color,
spaceId: Option.fromNullable(spaceId),
parentId: Option.none(),
}),
),
onSuccess: () => {
const createFolder = useEffectMutation({
mutationFn: (data: { name: string; color: FolderColor }) =>
withRpc((r) =>
r.FolderCreate({
name: data.name.trim(),
color: data.color,
spaceId: Option.fromNullable(spaceId),
parentId: Option.none(),
}),
),
onSuccess: () => {
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx around lines
67 to 77, the mutation payload uses an incorrect/loose color type and sends the
raw name; update the mutationFn signature to accept color as Folder.FolderColor
and send a trimmed name by calling data.name.trim() when building the
FolderCreate RPC payload (leave spaceId and parentId handling unchanged).

Comment on lines +64 to +87
create: Effect.fn("Folders.create")(function* (data: {
name: string;
color: Folder.FolderColor;
spaceId: Option.Option<string>;
parentId: Option.Option<Folder.FolderId>;
}) {
const user = yield* CurrentUser;

if (Option.isSome(data.parentId)) {
const parentId = data.parentId.value;
const [parentFolder] = yield* db.execute((db) =>
db
.select()
.from(Db.folders)
.where(
Dz.and(
Dz.eq(Db.folders.id, parentId),
Dz.eq(Db.folders.organizationId, user.activeOrgId),
),
),
);

if (!parentFolder) return yield* new Folder.NotFoundError();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate spaceId belongs to the active org (prevent cross-org references)

When creating a top-level folder in a space (no parentId), confirm the provided spaceId exists and is owned by user.activeOrgId. Otherwise, a folder could reference a space from another org.

Apply this diff after computing effectiveSpaceId and before insert:

+        // If creating a top-level folder in a space, ensure the space exists in the active org
+        if (Option.isNone(data.parentId) && Option.isSome(effectiveSpaceId)) {
+          const [space] = yield* db.execute((db) =>
+            db
+              .select({ id: Db.spaces.id })
+              .from(Db.spaces)
+              .where(
+                Dz.and(
+                  Dz.eq(Db.spaces.id, effectiveSpaceId.value),
+                  Dz.eq(Db.spaces.organizationId, user.activeOrgId),
+                ),
+              ),
+          );
+          if (!space) return yield* new Folder.NotFoundError();
+        }

If the schema uses a different table identifier than Db.spaces, adjust accordingly.

📝 Committable suggestion

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

Suggested change
create: Effect.fn("Folders.create")(function* (data: {
name: string;
color: Folder.FolderColor;
spaceId: Option.Option<string>;
parentId: Option.Option<Folder.FolderId>;
}) {
const user = yield* CurrentUser;
if (Option.isSome(data.parentId)) {
const parentId = data.parentId.value;
const [parentFolder] = yield* db.execute((db) =>
db
.select()
.from(Db.folders)
.where(
Dz.and(
Dz.eq(Db.folders.id, parentId),
Dz.eq(Db.folders.organizationId, user.activeOrgId),
),
),
);
if (!parentFolder) return yield* new Folder.NotFoundError();
}
const user = yield* CurrentUser;
// Use parent’s spaceId if creating a subfolder
let effectiveSpaceId = data.spaceId;
if (Option.isSome(data.parentId)) {
const parentId = data.parentId.value;
const [parentFolder] = yield* db.execute((db) =>
db
.select()
.from(Db.folders)
.where(
Dz.and(
Dz.eq(Db.folders.id, parentId),
Dz.eq(Db.folders.organizationId, user.activeOrgId),
),
),
);
if (!parentFolder) return yield* new Folder.NotFoundError();
effectiveSpaceId = Option.fromNullable(parentFolder.spaceId);
}
// If creating a top-level folder in a space, ensure the space exists in the active org
if (Option.isNone(data.parentId) && Option.isSome(effectiveSpaceId)) {
const [space] = yield* db.execute((db) =>
db
.select({ id: Db.spaces.id })
.from(Db.spaces)
.where(
Dz.and(
Dz.eq(Db.spaces.id, effectiveSpaceId.value),
Dz.eq(Db.spaces.organizationId, user.activeOrgId),
),
),
);
if (!space) return yield* new Folder.NotFoundError();
}
const folder = {
id: Folder.FolderId.make(nanoId()),
name: data.name,
color: data.color,
organizationId: user.activeOrgId,
createdById: user.id,
spaceId: effectiveSpaceId,
parentId: data.parentId,
};
yield* db.execute((db) =>
db.insert(Db.folders).values({
...folder,
spaceId: Option.getOrNull(effectiveSpaceId),
parentId: Option.getOrNull(folder.parentId),
}),
);
return new Folder.Folder(folder);

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

🧹 Nitpick comments (2)
packages/web-backend/src/Rpcs.ts (2)

25-34: Fix Biome formatting to unblock CI (arrow param parentheses and layout)

The CI is failing with a Biome formatting error on this block. Wrap the arrow function parameter in parentheses and let Biome reflow the object for consistency.

Apply this diff and run the formatter:

-          Option.match({
-            onNone: () => new UnauthenticatedError(),
-            onSome: user => Effect.succeed({
-              id: user.id,
-              email: user.email,
-              activeOrgId: user.activeOrganizationId,
-            }),
-          }),
+          Option.match({
+            onNone: () => new UnauthenticatedError(),
+            onSome: (user) =>
+              Effect.succeed({
+                id: user.id,
+                email: user.email,
+                activeOrgId: user.activeOrganizationId,
+              }),
+          }),

Alternatively, run: pnpm biome format to apply house style across the repo.


27-31: Guard against missing activeOrganizationId (if not guaranteed non-null)

If user.activeOrganizationId can be null/undefined, this will violate the declared type activeOrgId: string and propagate undefined to consumers. Consider failing the auth middleware when the user lacks an active org, or represent it explicitly as an Option in the auth context.

Example (fail when missing):

-            onSome: (user) =>
-              Effect.succeed({
-                id: user.id,
-                email: user.email,
-                activeOrgId: user.activeOrganizationId,
-              }),
+            onSome: (user) =>
+              user.activeOrganizationId
+                ? Effect.succeed({
+                    id: user.id,
+                    email: user.email,
+                    activeOrgId: user.activeOrganizationId,
+                  })
+                : Effect.fail(new UnauthenticatedError()),

If the domain guarantees activeOrganizationId is always set for authenticated users, please ignore and confirm that invariant.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3e46f43 and b924067.

📒 Files selected for processing (1)
  • packages/web-backend/src/Rpcs.ts (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: CI
packages/web-backend/src/Rpcs.ts

[error] 25-34: Biome formatting check failed: File content differs from formatting output.

⏰ 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). (1)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (1)
packages/web-backend/src/Rpcs.ts (1)

27-31: Addition of activeOrgId in auth context looks good

Mapping activeOrgId from user.activeOrganizationId aligns with the updated CurrentUser payload and enables org-scoped RPCs like FolderCreate. No functional concerns with this change.

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web-backend/src/Rpcs.ts (1)

21-24: catchAll should re-fail; currently returns a plain error value.

Effect.catchAll expects the handler to return an Effect. Returning new InternalError(...) makes the error a success value.

Minimal fix:

-        Effect.catchAll(() => new InternalError({ type: "database" })),
+        Effect.catchAll(() => Effect.fail(new InternalError({ type: "database" }))),

Optional: Prefer mapping the error instead of catching-and-failing:

-        Effect.catchAll(() => Effect.fail(new InternalError({ type: "database" }))),
+        Effect.mapError(() => new InternalError({ type: "database" })),
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b924067 and bbb55ab.

📒 Files selected for processing (1)
  • packages/web-backend/src/Rpcs.ts (1 hunks)
⏰ 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). (4)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
packages/web-backend/src/Rpcs.ts (1)

13-13: LGTM: clean RPC composition.

Merging VideosRpcsLive and FolderRpcsLive via Layer.mergeAll is straightforward and keeps RPC wiring centralized.

Comment on lines +28 to +32
Effect.succeed({
id: user.id,
email: user.email,
activeOrgId: user.activeOrganizationId,
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify downstream usage of the renamed activeOrgId field.

You’re now exposing activeOrgId (derived from user.activeOrganizationId). Ensure all callers/types consume activeOrgId and there are no lingering references to activeOrganizationId.

Run this script from repo root to find stale usages and confirm type expectations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for stale references to 'activeOrganizationId'..."
rg -nP -C2 '\bactiveOrganizationId\b' --type=ts --type=tsx

echo
echo "Consumers of 'activeOrgId'..."
rg -nP -C2 '\bactiveOrgId\b' --type=ts --type=tsx

echo
echo "Locate CurrentUser/RpcAuthMiddleware types to confirm shape..."
rg -nP -C3 '\b(CurrentUser|RpcAuthMiddleware)\b' --type=ts --type=tsx

Length of output: 243


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Searching for stale references to 'activeOrganizationId' in .ts/.tsx files..."
rg -nP -C2 '\bactiveOrganizationId\b' -g '*.ts' -g '*.tsx' || true

echo
echo "Searching for usages of 'activeOrgId' in .ts/.tsx files..."
rg -nP -C2 '\bactiveOrgId\b' -g '*.ts' -g '*.tsx' || true

echo
echo "Locating 'CurrentUser' type definitions in .ts/.tsx files..."
rg -nP -C3 '\b(interface|type)\s+CurrentUser\b' -g '*.ts' -g '*.tsx' || true

echo
echo "Locating 'RpcAuthMiddleware' references in .ts/.tsx files..."
rg -nP -C3 '\bRpcAuthMiddleware\b' -g '*.ts' -g '*.tsx' || true

Length of output: 23099


Update front-end references from activeOrganizationId to activeOrgId

The RPC layer now exposes activeOrgId (not activeOrganizationId) on the CurrentUser object. We still have numerous call sites in the front-end that will break or type-error. Please refactor all of these from user.activeOrganizationId (or currentUser.activeOrganizationId) to user.activeOrgId (or currentUser.activeOrgId):

• apps/web/app/s/[videoId]/page.tsx (lines ~525–528)
• apps/web/lib/folder.ts (lines ~229, 247)
• apps/web/lib/Notification.ts (lines ~39, 68, 102, 172–182)
• apps/web/app/api/notifications/route.ts (lines 56, 73)
• apps/web/actions/organization/* (upload-space-icon.ts, delete-space.ts, create-space.ts: lines 34, 75, 89)
• apps/web/actions/folders/* (updateFolder.ts, moveVideoToFolder.ts: lines ~21, 40)
• apps/web/components/forms/server.ts (line 110)
• apps/web/app/(org)/onboarding/page.tsx (line 19)
• apps/web/app/(org)/dashboard/**/* (Navbar/server.ts:35, layout.tsx:53, dashboard-data.ts:84–100, caps/page.tsx:132, 216)

Example replacements:

-   if (!user || !user.activeOrganizationId) throw …
+   if (!user || !user.activeOrgId) throw …
-   eq(folders.organizationId, user.activeOrganizationId)
+   eq(folders.organizationId, user.activeOrgId)
-   eq(notifications.orgId, currentUser.activeOrganizationId)
+   eq(notifications.orgId, currentUser.activeOrgId)

Use a global search (e.g. rg -t ts -t tsx '\bactiveOrganizationId\b') to catch all instances.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/web-backend/src/Rpcs.ts around lines 28–32 the RPC now returns
activeOrgId instead of activeOrganizationId; update the front-end to use
activeOrgId everywhere. Search the codebase (e.g. rg -t ts -t tsx
'\bactiveOrganizationId\b') and replace property accesses, destructurings and
type annotations from activeOrganizationId to activeOrgId in the listed files
(and any others found), update any client/server DTOs or types referencing the
old name, run the TypeScript build to catch remaining spots, and adjust tests
and imports accordingly so all references use activeOrgId consistently.

@Brendonovich Brendonovich merged commit bd3ba2a into main Aug 20, 2025
15 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Oct 23, 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