# Box Client

Upload, download, and manage files in Box.

## Methods

| Method                                   | Description                                     |
| ---------------------------------------- | ----------------------------------------------- |
| `apiRequest(options, schema, metadata?)` | Make any Box API request with schema validation |

## Usage

### List Files in a Folder

```typescript
import { api, z, box } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const PROD_BOX = "a1b2c3d4-5678-90ab-cdef-box000000001";

const ItemSchema = z.object({
  type: z.enum(["file", "folder", "web_link"]),
  id: z.string(),
  name: z.string(),
  sequence_id: z.string().nullable(),
  etag: z.string().nullable(),
});

const FileSchema = ItemSchema.extend({
  type: z.literal("file"),
  sha1: z.string().optional(),
  size: z.number().optional(),
  created_at: z.string().optional(),
  modified_at: z.string().optional(),
  content_created_at: z.string().optional(),
  content_modified_at: z.string().optional(),
});

const FolderSchema = ItemSchema.extend({
  type: z.literal("folder"),
});

const ListItemsResponseSchema = z.object({
  entries: z.array(z.union([FileSchema, FolderSchema, ItemSchema])),
  total_count: z.number(),
  offset: z.number(),
  limit: z.number(),
  order: z
    .array(
      z.object({
        by: z.string(),
        direction: z.string(),
      }),
    )
    .optional(),
});

export default api({
  name: "BoxExample",
  integrations: {
    box: box(PROD_BOX),
  },
  input: z.object({
    folderId: z.string().default("0"), // "0" is root folder
    limit: z.number().default(100),
  }),
  output: z.object({
    items: z.array(
      z.object({
        id: z.string(),
        name: z.string(),
        type: z.string(),
      }),
    ),
    totalCount: z.number(),
  }),
  async run(ctx, { folderId, limit }) {
    const result = await ctx.integrations.box.apiRequest(
      {
        method: "GET",
        path: `/folders/${folderId}/items`,
        params: {
          limit: limit,
          offset: 0,
          fields: "id,name,type,size,created_at,modified_at",
        },
      },
      { response: ListItemsResponseSchema },
    );

    return {
      items: result.entries.map((entry) => ({
        id: entry.id,
        name: entry.name,
        type: entry.type,
      })),
      totalCount: result.total_count,
    };
  },
});
```

### Get File Information

```typescript
const FileInfoSchema = z.object({
  type: z.literal("file"),
  id: z.string(),
  name: z.string(),
  size: z.number(),
  created_at: z.string(),
  modified_at: z.string(),
  created_by: z.object({
    id: z.string(),
    name: z.string(),
    login: z.string(),
  }),
  modified_by: z.object({
    id: z.string(),
    name: z.string(),
    login: z.string(),
  }),
  parent: z.object({
    id: z.string(),
    name: z.string(),
  }),
  sha1: z.string(),
  shared_link: z
    .object({
      url: z.string(),
      access: z.string(),
    })
    .nullable(),
});

const file = await ctx.integrations.box.apiRequest(
  {
    method: "GET",
    path: `/files/${fileId}`,
    params: {
      fields:
        "id,name,size,created_at,modified_at,created_by,modified_by,parent,sha1,shared_link",
    },
  },
  { response: FileInfoSchema },
);

console.log(`File: ${file.name} (${file.size} bytes)`);
```

### Download a File

```typescript
// Get download URL
const DownloadUrlResponseSchema = z.string();

const content = await ctx.integrations.box.apiRequest(
  {
    method: "GET",
    path: `/files/${fileId}/content`,
  },
  { response: DownloadUrlResponseSchema },
);
```

### Upload a File

```typescript
const UploadResponseSchema = z.object({
  entries: z.array(FileInfoSchema),
  total_count: z.number(),
});

// For files < 50MB, use direct upload
const result = await ctx.integrations.box.apiRequest(
  {
    method: "POST",
    path: "/files/content",
    headers: {
      "Content-Type": "multipart/form-data",
    },
    body: {
      attributes: JSON.stringify({
        name: "new-file.txt",
        parent: { id: folderId },
      }),
      file: fileContent,
    },
  },
  { response: UploadResponseSchema },
);

const uploadedFile = result.entries[0];
console.log(`Uploaded: ${uploadedFile.name} (${uploadedFile.id})`);
```

### Create a Folder

```typescript
const FolderInfoSchema = z.object({
  type: z.literal("folder"),
  id: z.string(),
  name: z.string(),
  created_at: z.string(),
  modified_at: z.string(),
  parent: z.object({
    id: z.string(),
    name: z.string(),
  }),
});

const folder = await ctx.integrations.box.apiRequest(
  {
    method: "POST",
    path: "/folders",
    body: {
      name: "New Folder",
      parent: {
        id: parentFolderId, // Use "0" for root
      },
    },
  },
  { response: FolderInfoSchema },
);

console.log(`Created folder: ${folder.name} (${folder.id})`);
```

### Copy a File

```typescript
const copiedFile = await ctx.integrations.box.apiRequest(
  {
    method: "POST",
    path: `/files/${fileId}/copy`,
    body: {
      parent: {
        id: destinationFolderId,
      },
      name: "Copy of file.txt", // Optional new name
    },
  },
  { response: FileInfoSchema },
);
```

### Move a File

```typescript
const movedFile = await ctx.integrations.box.apiRequest(
  {
    method: "PUT",
    path: `/files/${fileId}`,
    body: {
      parent: {
        id: newParentFolderId,
      },
    },
  },
  { response: FileInfoSchema },
);
```

### Update File Information

```typescript
const updatedFile = await ctx.integrations.box.apiRequest(
  {
    method: "PUT",
    path: `/files/${fileId}`,
    body: {
      name: "renamed-file.txt",
      description: "Updated file description",
      tags: ["important", "quarterly"],
    },
  },
  { response: FileInfoSchema },
);
```

### Delete a File

```typescript
await ctx.integrations.box.apiRequest(
  {
    method: "DELETE",
    path: `/files/${fileId}`,
  },
  { response: z.void() },
);
```

### Create a Shared Link

```typescript
const SharedLinkSchema = z.object({
  url: z.string(),
  download_url: z.string().optional(),
  access: z.string(),
  effective_access: z.string(),
  permissions: z.object({
    can_download: z.boolean(),
    can_preview: z.boolean(),
  }),
});

const file = await ctx.integrations.box.apiRequest(
  {
    method: "PUT",
    path: `/files/${fileId}`,
    params: {
      fields: "shared_link",
    },
    body: {
      shared_link: {
        access: "open", // open, company, collaborators
        permissions: {
          can_download: true,
          can_preview: true,
        },
      },
    },
  },
  { response: z.object({ shared_link: SharedLinkSchema }) },
);

console.log(`Share URL: ${file.shared_link.url}`);
```

### Search Files

```typescript
const SearchResponseSchema = z.object({
  entries: z.array(z.union([FileSchema, FolderSchema])),
  total_count: z.number(),
  offset: z.number(),
  limit: z.number(),
});

const result = await ctx.integrations.box.apiRequest(
  {
    method: "GET",
    path: "/search",
    params: {
      query: "quarterly report",
      type: "file",
      file_extensions: "pdf,docx",
      ancestor_folder_ids: folderId,
      limit: 25,
      fields: "id,name,type,size,created_at",
    },
  },
  { response: SearchResponseSchema },
);

result.entries.forEach((entry) => {
  console.log(`Found: ${entry.name}`);
});
```

### Add a Comment

```typescript
const CommentSchema = z.object({
  id: z.string(),
  type: z.literal("comment"),
  message: z.string(),
  created_by: z.object({
    id: z.string(),
    name: z.string(),
  }),
  created_at: z.string(),
});

const comment = await ctx.integrations.box.apiRequest(
  {
    method: "POST",
    path: "/comments",
    body: {
      item: {
        type: "file",
        id: fileId,
      },
      message: "Great work on this document!",
    },
  },
  { response: CommentSchema },
);
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### No Specialized Methods

```typescript
// WRONG - These methods do not exist
await box.listFiles({ ... });
await box.uploadFile({ ... });

// CORRECT - Use apiRequest
await ctx.integrations.box.apiRequest(
  { method: "GET", path: `/folders/${folderId}/items`, params: { ... } },
  { response: ListItemsResponseSchema }
);
```

### API Version in Path

The base URL already includes `/2.0`, so do not repeat it in paths:

```typescript
// CORRECT - No version prefix (base URL already has /2.0)
const path = "/files/123";
const path = "/folders/0/items";

// WRONG - Duplicates version from base URL
const path = "/2.0/files/123";
```

### Root Folder ID is "0"

The root folder has ID "0", not "root":

```typescript
// CORRECT - Root folder
const folderId = "0";

// WRONG - "root" is not valid
const folderId = "root";
```

### Fields Parameter

Use `fields` to get specific data:

```typescript
// Without fields, you get minimal data
const result = await ctx.integrations.box.apiRequest(
  { method: "GET", path: `/files/${fileId}` },
  { response: FileSchema },
);

// With fields, specify what you need
const result = await ctx.integrations.box.apiRequest(
  {
    method: "GET",
    path: `/files/${fileId}`,
    params: {
      fields: "id,name,size,created_at,modified_at,parent,shared_link",
    },
  },
  { response: FileInfoSchema },
);
```

### Upload Returns Array

File uploads return an array in `entries`:

```typescript
// Response structure
const schema = z.object({
  entries: z.array(FileInfoSchema), // Array even for single file
  total_count: z.number(),
});

// Access the uploaded file
const uploadedFile = result.entries[0];
```

### If-Match Header for Updates

Use `If-Match` with etag for safe updates:

```typescript
const result = await ctx.integrations.box.apiRequest(
  {
    method: "PUT",
    path: `/files/${fileId}`,
    headers: {
      "If-Match": file.etag, // Prevents overwriting concurrent changes
    },
    body: {
      name: "new-name.txt",
    },
  },
  { response: FileInfoSchema },
);
```

### Pagination

Box uses offset-based pagination:

```typescript
async function getAllItems(box: BoxClient, folderId: string) {
  const allItems: Item[] = [];
  const limit = 100;
  let offset = 0;

  while (true) {
    const result = await ctx.integrations.box.apiRequest(
      {
        method: "GET",
        path: `/folders/${folderId}/items`,
        params: { limit, offset, fields: "id,name,type" },
      },
      { response: ListItemsResponseSchema },
    );

    allItems.push(...result.entries);
    if (offset + result.entries.length >= result.total_count) break;
    offset += limit;
  }

  return allItems;
}
```

### File Size Limits

Different upload methods for different file sizes:

```typescript
// < 50MB - Use /files/content (simple upload)
// > 50MB - Use chunked upload session

// Chunked upload for large files:
// 1. POST /files/upload_sessions - Create session
// 2. PUT /files/upload_sessions/:id - Upload parts
// 3. POST /files/upload_sessions/:id/commit - Finalize
```

### Shared Link Access Levels

```typescript
const accessLevels = {
  open: "Anyone with the link",
  company: "Anyone in the enterprise",
  collaborators: "Only collaborators",
};

const body = {
  shared_link: {
    access: "company",
    password: "optional-password", // Optional
    unshared_at: "2024-12-31T23:59:59Z", // Optional expiration
  },
};
```

## Error Handling

```typescript
import { RestApiValidationError } from "@superblocksteam/sdk-api";

try {
  const result = await ctx.integrations.box.apiRequest(
    { method: "GET", path: `/folders/${folderId}/items`, params: { ... } },
    { response: ListItemsResponseSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [Box API Documentation](https://developer.box.com/reference/)
- [Files](https://developer.box.com/reference/resources/file/)
- [Folders](https://developer.box.com/reference/resources/folder/)
- [Shared Links](https://developer.box.com/guides/shared-links/)
- [Search](https://developer.box.com/reference/get-search/)
