Skip to content

Corpus Folders API Reference for Frontend

Quick Start

The corpus folders API provides hierarchical organization for documents within corpuses. This document focuses on the GraphQL API surface and integration points for frontend developers.


GraphQL Types

CorpusFolderType

type CorpusFolderType {
  # Standard fields from AnnotatePermissionsForReadMixin
  id: ID!
  myPermissions: [String!]!       # ["create_corpus", "read_corpus", "update_corpus", etc.]
  isPublished: Boolean!
  objectSharedWith: [UserType!]!

  # Folder-specific fields
  name: String!                    # Folder name (not full path)
  description: String!             # Optional description
  color: String!                   # Hex color code (default: "#05313d")
  icon: String!                    # Icon identifier (default: "folder")
  tags: GenericScalar!             # JSON array of tag strings
  isPublic: Boolean!
  created: DateTime!
  modified: DateTime!

  # Relationships
  corpus: CorpusType!
  parent: CorpusFolderType         # null if root-level folder
  creator: UserType!

  # Computed fields
  path: String!                    # Full path from root (e.g., "Legal/Contracts/2024")
  documentCount: Int!              # Documents directly in this folder
  descendantDocumentCount: Int!   # Documents in this folder + all subfolders
  children: [CorpusFolderType!]!  # Immediate child folders
}

Important Notes: - tags returns a JSON string like "[\"tag1\", \"tag2\"]" - parse with JSON.parse() if needed - myPermissions inherits from parent corpus (folders don't have separate permissions) - path is a computed field showing the full folder path with "/" separators

DocumentType (New Field)

type DocumentType {
  # ... existing fields ...

  # NEW: Get folder assignment for this document in a specific corpus
  folderInCorpus(corpusId: ID!): CorpusFolderType  # null = document is in root
}

CorpusType (New Field)

type CorpusType {
  # ... existing fields ...

  # NEW: All folders in this corpus
  folders: [CorpusFolderType!]!
}

Queries

1. Get All Folders in a Corpus

Query:

query GetCorpusFolders($corpusId: ID!) {
  corpusFolders(corpusId: $corpusId) {
    id
    name
    description
    color
    icon
    tags
    path
    documentCount
    descendantDocumentCount
    created
    modified
    parent {
      id
      name
    }
    myPermissions
    isPublished
  }
}

Response:

{
  "data": {
    "corpusFolders": [
      {
        "id": "Q29ycHVzRm9sZGVyVHlwZTox",
        "name": "Legal",
        "description": "Legal documents",
        "color": "#ff0000",
        "icon": "folder",
        "tags": "[\"important\"]",
        "path": "Legal",
        "documentCount": 5,
        "descendantDocumentCount": 23,
        "parent": null,
        "myPermissions": ["read_corpus", "update_corpus"],
        "isPublished": false
      },
      {
        "id": "Q29ycHVzRm9sZGVyVHlwZToy",
        "name": "Contracts",
        "path": "Legal/Contracts",
        "parent": {
          "id": "Q29ycHVzRm9sZGVyVHlwZTox",
          "name": "Legal"
        },
        // ... other fields
      }
    ]
  }
}

Usage: - Returns a flat list of all folders - Frontend builds tree structure from parent relationships - Use this query to populate the folder tree sidebar - Returns only folders user has permission to view

TypeScript Example:

const { data, loading } = useQuery(GET_CORPUS_FOLDERS, {
  variables: { corpusId: selectedCorpusId }
});

// Build tree from flat list
const folderTree = buildTreeFromFlatList(data?.corpusFolders || []);

2. Get Single Folder

Query:

query GetCorpusFolder($id: ID!) {
  corpusFolder(id: $id) {
    id
    name
    path
    parent {
      id
      name
    }
    children {
      id
      name
      documentCount
    }
    # ... other fields
  }
}

Usage: - Get detailed info about a specific folder - Includes immediate children - Use for folder detail views or breadcrumb construction

3. Get Documents in a Folder

Query:

query GetDocumentsInFolder(
  $corpusId: ID!
  $folderId: String  # Use "__root__" for root documents, or folder global ID
  $limit: Int
  $offset: Int
) {
  documents(
    corpusId: $corpusId
    inFolderId: $folderId
    limit: $limit
    offset: $offset
  ) {
    edges {
      node {
        id
        title
        description
        # ... other document fields
      }
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
    }
    totalCount
  }
}

Special Values for inFolderId: - null or omitted: All documents in corpus (no filtering) - "__root__": Documents not assigned to any folder (in corpus root) - Folder global ID: Documents in that specific folder

Usage:

// Get root documents
const { data } = useQuery(GET_DOCUMENTS, {
  variables: {
    corpusId: selectedCorpusId,
    inFolderId: "__root__"
  }
});

// Get documents in specific folder
const { data } = useQuery(GET_DOCUMENTS, {
  variables: {
    corpusId: selectedCorpusId,
    inFolderId: selectedFolderId
  }
});


Mutations

1. Create Folder

Mutation:

mutation CreateCorpusFolder(
  $corpusId: ID!
  $name: String!
  $parentId: ID           # Omit for root-level folder
  $description: String
  $color: String
  $icon: String
  $tags: [String]
) {
  createCorpusFolder(
    corpusId: $corpusId
    name: $name
    parentId: $parentId
    description: $description
    color: $color
    icon: $icon
    tags: $tags
  ) {
    ok
    message
    folder {
      id
      name
      path
      parent {
        id
      }
    }
  }
}

Example Variables:

{
  "corpusId": "Q29ycHVzVHlwZTox",
  "name": "2024 Contracts",
  "parentId": "Q29ycHVzRm9sZGVyVHlwZToy",
  "description": "All contracts from 2024",
  "color": "#3498db",
  "icon": "folder",
  "tags": ["2024", "active"]
}

Success Response:

{
  "data": {
    "createCorpusFolder": {
      "ok": true,
      "message": "Folder created successfully",
      "folder": {
        "id": "Q29ycHVzRm9sZGVyVHlwZTo1",
        "name": "2024 Contracts",
        "path": "Legal/Contracts/2024 Contracts",
        "parent": {
          "id": "Q29ycHVzRm9sZGVyVHlwZToy"
        }
      }
    }
  }
}

Error Response:

{
  "data": {
    "createCorpusFolder": {
      "ok": false,
      "message": "A folder named '2024 Contracts' already exists in this location",
      "folder": null
    }
  }
}

Permission Required: User must be corpus creator, corpus is public, or user has UPDATE permission on corpus

2. Update Folder

Mutation:

mutation UpdateCorpusFolder(
  $folderId: ID!
  $name: String
  $description: String
  $color: String
  $icon: String
  $tags: [String]
) {
  updateCorpusFolder(
    folderId: $folderId
    name: $name
    description: $description
    color: $color
    icon: $icon
    tags: $tags
  ) {
    ok
    message
    folder {
      id
      name
      description
      color
      icon
      tags
    }
  }
}

Notes: - All fields except folderId are optional - Only provided fields will be updated - Cannot change parent (use moveCorpusFolder instead)

Permission Required: UPDATE on corpus

3. Move Folder

Mutation:

mutation MoveCorpusFolder(
  $folderId: ID!
  $newParentId: ID  # null to move to root
) {
  moveCorpusFolder(
    folderId: $folderId
    newParentId: $newParentId
  ) {
    ok
    message
    folder {
      id
      name
      path
      parent {
        id
        name
      }
    }
  }
}

Usage:

// Move folder to root
await moveFolder({
  variables: {
    folderId: draggedFolderId,
    newParentId: null
  }
});

// Move folder under another folder
await moveFolder({
  variables: {
    folderId: draggedFolderId,
    newParentId: targetFolderId
  }
});

Validation: - Cannot move folder into itself - Cannot move folder into its own descendants (circular reference) - Cannot move folder to different corpus - Duplicate names under new parent will fail

Permission Required: UPDATE on corpus

4. Delete Folder

Mutation:

mutation DeleteCorpusFolder(
  $folderId: ID!
  $deleteContents: Boolean  # Default: false
) {
  deleteCorpusFolder(
    folderId: $folderId
    deleteContents: $deleteContents
  ) {
    ok
    message
  }
}

Behavior: - deleteContents: false (default): - Moves child folders to this folder's parent - Moves documents to corpus root (deletes CorpusDocumentFolder records)

  • deleteContents: true:
  • Deletes folder and all subfolders (CASCADE)
  • Documents remain in corpus but move to root

Permission Required: DELETE on corpus

5. Move Document to Folder

Mutation:

mutation MoveDocumentToFolder(
  $documentId: ID!
  $corpusId: ID!
  $folderId: ID  # null to move to root
) {
  moveDocumentToFolder(
    documentId: $documentId
    corpusId: $corpusId
    folderId: $folderId
  ) {
    ok
    message
    document {
      id
      title
    }
  }
}

Usage:

// Move document to folder
await moveDocument({
  variables: {
    documentId: doc.id,
    corpusId: currentCorpusId,
    folderId: targetFolderId
  }
});

// Move document to corpus root
await moveDocument({
  variables: {
    documentId: doc.id,
    corpusId: currentCorpusId,
    folderId: null
  }
});

Notes: - Replaces any existing folder assignment for this document in this corpus - Document must already be in the corpus - Folder must be in the same corpus

Permission Required: UPDATE on corpus

6. Bulk Move Documents to Folder

Mutation:

mutation MoveDocumentsToFolder(
  $documentIds: [ID]!
  $corpusId: ID!
  $folderId: ID  # null to move to root
) {
  moveDocumentsToFolder(
    documentIds: $documentIds
    corpusId: $corpusId
    folderId: $folderId
  ) {
    ok
    message
    movedCount
  }
}

Usage:

// Bulk move selected documents
await bulkMoveDocuments({
  variables: {
    documentIds: selectedDocumentIds,
    corpusId: currentCorpusId,
    folderId: targetFolderId
  }
});

Response:

{
  "data": {
    "moveDocumentsToFolder": {
      "ok": true,
      "message": "Successfully moved 15 document(s)",
      "movedCount": 15
    }
  }
}

Permission Required: UPDATE on corpus


Integration Patterns

1. Building Folder Tree from Flat List

interface CorpusFolder {
  id: string;
  name: string;
  parent: { id: string } | null;
  children?: CorpusFolder[];
}

function buildFolderTree(folders: CorpusFolder[]): CorpusFolder[] {
  const folderMap = new Map<string, CorpusFolder>();
  const rootFolders: CorpusFolder[] = [];

  // First pass: create map and initialize children arrays
  folders.forEach(folder => {
    folderMap.set(folder.id, { ...folder, children: [] });
  });

  // Second pass: build tree structure
  folders.forEach(folder => {
    const node = folderMap.get(folder.id)!;

    if (folder.parent) {
      const parentNode = folderMap.get(folder.parent.id);
      if (parentNode) {
        parentNode.children!.push(node);
      }
    } else {
      rootFolders.push(node);
    }
  });

  return rootFolders;
}

2. Building Breadcrumb Path

function buildBreadcrumbPath(
  folderId: string | null,
  folders: CorpusFolder[]
): CorpusFolder[] {
  if (!folderId) return [];

  const folderMap = new Map(folders.map(f => [f.id, f]));
  const path: CorpusFolder[] = [];

  let current = folderMap.get(folderId);
  while (current) {
    path.unshift(current);
    current = current.parent ? folderMap.get(current.parent.id) : null;
  }

  return path;
}

3. Optimistic UI Updates

// When creating a folder
const [createFolder] = useMutation(CREATE_CORPUS_FOLDER, {
  update(cache, { data: { createCorpusFolder } }) {
    if (createCorpusFolder.ok) {
      // Add to cache
      cache.modify({
        fields: {
          corpusFolders(existingFolders = []) {
            const newFolderRef = cache.writeFragment({
              data: createCorpusFolder.folder,
              fragment: gql`
                fragment NewFolder on CorpusFolderType {
                  id
                  name
                  parent { id }
                }
              `
            });
            return [...existingFolders, newFolderRef];
          }
        }
      });
    }
  },
  optimisticResponse: {
    createCorpusFolder: {
      __typename: 'CreateCorpusFolderMutation',
      ok: true,
      message: 'Creating...',
      folder: {
        __typename: 'CorpusFolderType',
        id: `temp-${Date.now()}`,
        name: variables.name,
        path: `.../${variables.name}`,
        parent: variables.parentId ? { id: variables.parentId } : null,
        // ... other fields with sensible defaults
      }
    }
  }
});

4. Permission Checking

function canUserManageFolders(folder: CorpusFolder): boolean {
  const permissions = folder.myPermissions;
  return permissions.includes('update_corpus') ||
         permissions.includes('create_corpus');
}

function canUserDeleteFolder(folder: CorpusFolder): boolean {
  return folder.myPermissions.includes('delete_corpus');
}

// In component
{canUserManageFolders(folder) && (
  <button onClick={() => openEditModal(folder)}>
    Edit Folder
  </button>
)}

5. Drag and Drop Document Assignment

const [moveDocument] = useMutation(MOVE_DOCUMENT_TO_FOLDER);

const handleDocumentDrop = async (
  documentId: string,
  targetFolderId: string | null
) => {
  try {
    const result = await moveDocument({
      variables: {
        documentId,
        corpusId: currentCorpusId,
        folderId: targetFolderId
      },
      // Refetch documents in both source and target folders
      refetchQueries: ['GetDocumentsInFolder']
    });

    if (result.data.moveDocumentToFolder.ok) {
      toast.success('Document moved successfully');
    }
  } catch (error) {
    toast.error('Failed to move document');
  }
};

Error Handling

All mutations return a consistent structure:

interface MutationResponse {
  ok: boolean;
  message: string;
  // ... other fields
}

Common Error Messages:

Message Meaning Solution
"You do not have permission to..." User lacks required permission Check corpus permissions
"A folder named 'X' already exists in this location" Duplicate name under same parent Choose different name or parent
"Folder not found" Folder doesn't exist or user can't access Check folder ID and permissions
"Corpus not found" Corpus doesn't exist or user can't access Check corpus ID and permissions
"Cannot move folder into itself or its descendants" Circular reference attempt Choose different target parent
"Parent folder must be in the same corpus" Cross-corpus move attempt Keep folders within same corpus
"Document is not in this corpus" Document not added to corpus Add document to corpus first

Rate Limiting

All mutations use rate limiting: - CREATE, UPDATE, MOVE operations: RateLimits.WRITE_LIGHT - DELETE operations: RateLimits.WRITE_LIGHT - BULK operations: RateLimits.WRITE_HEAVY

In production, rate limits are enforced. In tests, rate limiting is skipped for TestContext.


Apollo Cache Considerations

Automatic Cache Updates

These happen automatically: - Fetching a single folder updates the cache entry - Fetching folder list updates all folder entries

Manual Cache Updates Needed

Handle these in mutation update functions: - Creating a folder → add to corpusFolders list - Deleting a folder → remove from list - Moving a folder → update parent relationships - Moving documents → refetch affected folder document counts

Example Cache Update

const [deleteFolder] = useMutation(DELETE_CORPUS_FOLDER, {
  update(cache, { data }) {
    if (data.deleteCorpusFolder.ok) {
      cache.modify({
        fields: {
          corpusFolders(existingFolders, { readField }) {
            return existingFolders.filter(
              folderRef => readField('id', folderRef) !== deletedFolderId
            );
          }
        }
      });

      // Also evict the folder itself
      cache.evict({ id: cache.identify({
        __typename: 'CorpusFolderType',
        id: deletedFolderId
      })});
      cache.gc();
    }
  }
});

Testing Utilities

Mock Data Factory

function createMockFolder(overrides?: Partial<CorpusFolder>): CorpusFolder {
  return {
    id: `mock-folder-${Math.random()}`,
    name: 'Test Folder',
    description: 'Test description',
    color: '#05313d',
    icon: 'folder',
    tags: '[]',
    path: 'Test Folder',
    documentCount: 0,
    descendantDocumentCount: 0,
    created: new Date().toISOString(),
    modified: new Date().toISOString(),
    parent: null,
    children: [],
    myPermissions: ['read_corpus', 'update_corpus'],
    isPublished: false,
    ...overrides
  };
}

Mock Apollo Provider

const mocks = [
  {
    request: {
      query: GET_CORPUS_FOLDERS,
      variables: { corpusId: 'test-corpus-id' }
    },
    result: {
      data: {
        corpusFolders: [
          createMockFolder({ name: 'Folder 1' }),
          createMockFolder({ name: 'Folder 2', parent: { id: 'folder-1-id' } })
        ]
      }
    }
  }
];

<MockedProvider mocks={mocks} addTypename={false}>
  <YourComponent />
</MockedProvider>

Quick Reference: Mutation Permissions

Mutation Permission Required
createCorpusFolder corpus.creator OR corpus.is_public OR UPDATE permission
updateCorpusFolder corpus.creator OR corpus.is_public OR UPDATE permission
moveCorpusFolder corpus.creator OR corpus.is_public OR UPDATE permission
deleteCorpusFolder corpus.creator OR DELETE permission
moveDocumentToFolder corpus.creator OR corpus.is_public OR UPDATE permission
moveDocumentsToFolder corpus.creator OR corpus.is_public OR UPDATE permission

Key Points: - Folders have NO separate permissions - they inherit from corpus - myPermissions field shows effective permissions (from corpus) - Public corpuses allow any authenticated user to manage folders - DELETE permission is stricter (no is_public bypass)


Common Workflows

1. Initial Load: Populate Folder Tree

1. User selects corpus
2. Query: corpusFolders(corpusId)
3. Build tree from flat list
4. Render FolderTreeSidebar
5. Load documents for root folder

2. Navigate to Folder

1. User clicks folder in tree
2. Update selectedFolderIdAtom
3. Update URL: /corpus/:corpusId/folder/:folderId
4. Query: documents(corpusId, inFolderId)
5. Render FolderBreadcrumb + CorpusDocumentCards

3. Create Subfolder

1. User right-clicks folder → "Create Subfolder"
2. Show CreateFolderModal with parentId pre-filled
3. User fills form
4. Mutation: createCorpusFolder(parentId: selectedFolderId)
5. Update cache, refetch if needed
6. Expand parent in tree to show new child

4. Drag Document to Folder

1. User drags document from list
2. User drops on folder in tree
3. Mutation: moveDocumentToFolder(documentId, folderId)
4. Refetch document list for both folders
5. Update folder document counts

5. Reorganize Folders

1. User drags folder in tree
2. User drops on target parent folder
3. Validate: not dropping into self/descendant
4. Mutation: moveCorpusFolder(newParentId)
5. Update tree structure
6. Update breadcrumb if currently viewing moved folder

Last Updated: 2025-11-10 Backend Version: v3.0.0.b3 Status: Production Ready ✅