# Confluence Client

Create pages, manage content, and search in Confluence spaces.

## Methods

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

## Usage

### Create a Page

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

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

const PageSchema = z.object({
  id: z.string(),
  type: z.string(),
  status: z.string(),
  title: z.string(),
  _links: z.object({
    webui: z.string(),
    self: z.string(),
  }),
});

export default api({
  name: "ConfluenceExample",
  integrations: {
    confluence: confluence(PROD_CONFLUENCE),
  },
  input: z.object({
    spaceKey: z.string(),
    title: z.string(),
    content: z.string(),
  }),
  output: z.object({
    pageId: z.string(),
    url: z.string(),
  }),
  async run(ctx, { spaceKey, title, content }) {
    const result = await ctx.integrations.confluence.apiRequest(
      {
        method: "POST",
        path: "/wiki/api/v2/pages",
        body: {
          spaceId: spaceKey, // Space ID
          status: "current",
          title: title,
          body: {
            representation: "storage",
            value: `<p>${content}</p>`,
          },
        },
      },
      { response: PageSchema },
    );

    return { pageId: result.id, url: result._links.webui };
  },
});
```

### Get a Page

```typescript
const PageDetailSchema = z.object({
  id: z.string(),
  title: z.string(),
  status: z.string(),
  body: z
    .object({
      storage: z
        .object({
          value: z.string(),
          representation: z.string(),
        })
        .optional(),
    })
    .optional(),
  version: z.object({
    number: z.number(),
    createdAt: z.string(),
  }),
});

const result = await ctx.integrations.confluence.apiRequest(
  {
    method: "GET",
    path: `/wiki/api/v2/pages/${pageId}`,
    params: {
      "body-format": "storage",
    },
  },
  { response: PageDetailSchema },
);

console.log(`Page: ${result.title} (v${result.version.number})`);
```

### Update a Page

```typescript
// First, get current version
const page = await ctx.integrations.confluence.apiRequest(
  { method: "GET", path: `/wiki/api/v2/pages/${pageId}` },
  { response: PageDetailSchema },
);

// Then update with incremented version
await ctx.integrations.confluence.apiRequest(
  {
    method: "PUT",
    path: `/wiki/api/v2/pages/${pageId}`,
    body: {
      id: pageId,
      status: "current",
      title: "Updated Title",
      body: {
        representation: "storage",
        value: "<p>Updated content</p>",
      },
      version: {
        number: page.version.number + 1, // Increment version
        message: "Updated via API",
      },
    },
  },
  { response: PageSchema },
);
```

### Search Content

```typescript
const SearchResponseSchema = z.object({
  results: z.array(
    z.object({
      content: z.object({
        id: z.string(),
        type: z.string(),
        title: z.string(),
      }),
      excerpt: z.string().optional(),
    }),
  ),
  totalSize: z.number(),
  _links: z.object({
    next: z.string().optional(),
  }),
});

const result = await ctx.integrations.confluence.apiRequest(
  {
    method: "GET",
    path: "/wiki/rest/api/search",
    params: {
      cql: 'type=page AND space="PROJ" AND text~"search term"',
      limit: 25,
    },
  },
  { response: SearchResponseSchema },
);

result.results.forEach((r) => {
  console.log(`${r.content.title}: ${r.excerpt}`);
});
```

### List Pages in a Space

```typescript
const ListPagesResponseSchema = z.object({
  results: z.array(
    z.object({
      id: z.string(),
      title: z.string(),
      status: z.string(),
    }),
  ),
  _links: z.object({
    next: z.string().optional(),
  }),
});

const result = await ctx.integrations.confluence.apiRequest(
  {
    method: "GET",
    path: `/wiki/api/v2/spaces/${spaceId}/pages`,
    params: {
      limit: 50,
      status: "current",
    },
  },
  { response: ListPagesResponseSchema },
);
```

### Create a Child Page

```typescript
const result = await ctx.integrations.confluence.apiRequest(
  {
    method: "POST",
    path: "/wiki/api/v2/pages",
    body: {
      spaceId: spaceId,
      parentId: parentPageId, // Parent page ID
      status: "current",
      title: "Child Page Title",
      body: {
        representation: "storage",
        value: "<p>Child page content</p>",
      },
    },
  },
  { response: PageSchema },
);
```

### Add a Comment

```typescript
const CommentSchema = z.object({
  id: z.string(),
  body: z.object({
    storage: z.object({
      value: z.string(),
    }),
  }),
});

const result = await ctx.integrations.confluence.apiRequest(
  {
    method: "POST",
    path: `/wiki/api/v2/pages/${pageId}/footer-comments`,
    body: {
      body: {
        representation: "storage",
        value: "<p>This is a comment</p>",
      },
    },
  },
  { response: CommentSchema },
);
```

### Get Space

```typescript
const SpaceSchema = z.object({
  id: z.string(),
  key: z.string(),
  name: z.string(),
  type: z.string(),
  status: z.string(),
});

const result = await ctx.integrations.confluence.apiRequest(
  {
    method: "GET",
    path: `/wiki/api/v2/spaces/${spaceId}`,
  },
  { response: SpaceSchema },
);
```

### Add Labels to a Page

```typescript
const LabelSchema = z.object({
  results: z.array(
    z.object({
      id: z.string(),
      name: z.string(),
    }),
  ),
});

const result = await ctx.integrations.confluence.apiRequest(
  {
    method: "POST",
    path: `/wiki/rest/api/content/${pageId}/label`,
    body: [{ name: "documentation" }, { name: "api" }],
  },
  { response: LabelSchema },
);
```

## 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 confluence.createPage({ ... });
await confluence.search({ ... });

// CORRECT - Use apiRequest
await ctx.integrations.confluence.apiRequest(
  { method: "POST", path: "/wiki/api/v2/pages", body: { ... } },
  { response: PageSchema }
);
```

### API Version (v2 vs REST)

Confluence has two API versions:

```typescript
// V2 API (newer, recommended)
const v2Path = "/wiki/api/v2/pages";

// REST API (older, some features only here)
const restPath = "/wiki/rest/api/content";

// Search is only in REST API
const searchPath = "/wiki/rest/api/search";
```

### Storage Format for Content

Page content uses Confluence storage format (XHTML-like):

```typescript
const body = {
  representation: "storage",
  value: `
    <h1>Heading</h1>
    <p>Paragraph with <strong>bold</strong> and <em>italic</em></p>
    <ul>
      <li>List item 1</li>
      <li>List item 2</li>
    </ul>
    <ac:structured-macro ac:name="code">
      <ac:parameter ac:name="language">javascript</ac:parameter>
      <ac:plain-text-body><![CDATA[console.log('Hello');]]></ac:plain-text-body>
    </ac:structured-macro>
  `,
};
```

### Version Number Required for Updates

Updates require incrementing the version number:

```typescript
// WRONG - Missing version
const body = {
  title: "New Title",
  body: { ... },
};

// CORRECT - Include incremented version
const body = {
  version: {
    number: currentVersion + 1, // Must increment
    message: "Update message",
  },
  title: "New Title",
  body: { ... },
};
```

### Space ID vs Space Key

V2 API uses space IDs, REST API uses space keys:

```typescript
// V2 API - uses spaceId
const v2Body = {
  spaceId: "123456", // Numeric ID
};

// REST API - uses spaceKey
const restBody = {
  space: { key: "PROJ" }, // Key string
};
```

### CQL Syntax (Confluence Query Language)

```typescript
// Basic search
const cql = 'type=page AND text~"search term"';

// Space filter
const cql = 'type=page AND space="PROJ"';

// Created/modified date
const cql = 'created >= now("-7d")'; // Last 7 days
const cql = 'lastModified >= "2024-01-01"';

// Label filter
const cql = 'label = "documentation"';

// Creator filter
const cql = "creator = currentUser()";

// Ancestor (pages under a specific parent)
const cql = "ancestor = 123456";

// Combine conditions
const cql =
  'type=page AND space="PROJ" AND label="api" ORDER BY lastModified DESC';
```

### Pagination

Large result sets require pagination:

```typescript
async function getAllPages(confluence: ConfluenceClient, spaceId: string) {
  const allPages: Page[] = [];
  let cursor: string | undefined;

  while (true) {
    const result = await ctx.integrations.confluence.apiRequest(
      {
        method: "GET",
        path: `/wiki/api/v2/spaces/${spaceId}/pages`,
        params: {
          limit: 100,
          ...(cursor && { cursor }),
        },
      },
      { response: ListPagesResponseSchema },
    );

    allPages.push(...result.results);

    if (!result._links.next) break;
    // Extract cursor from next link
    cursor =
      new URL(result._links.next).searchParams.get("cursor") ?? undefined;
  }

  return allPages;
}
```

## Error Handling

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

try {
  const result = await ctx.integrations.confluence.apiRequest(
    { method: "POST", path: "/wiki/api/v2/pages", body: { ... } },
    { response: PageSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [Confluence Cloud REST API (V2)](https://developer.atlassian.com/cloud/confluence/rest/v2/intro/)
- [Confluence Cloud REST API (V1)](https://developer.atlassian.com/cloud/confluence/rest/v1/intro/)
- [CQL Reference](https://developer.atlassian.com/cloud/confluence/advanced-searching-using-cql/)
- [Storage Format](https://confluence.atlassian.com/doc/confluence-storage-format-790796544.html)
