Skip to content

Commit 7c4b925

Browse files
committed
feat(confluence): added list space labels, delete label, delete page prop (#3201)
1 parent 5f3e76d commit 7c4b925

File tree

11 files changed

+983
-2
lines changed

11 files changed

+983
-2
lines changed

apps/docs/content/docs/en/tools/confluence.mdx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,28 @@ Create a new custom property (metadata) on a Confluence page.
399399
|`authorId` | string | Account ID of the version author |
400400
|`createdAt` | string | ISO 8601 timestamp of version creation |
401401

402+
### `confluence_delete_page_property`
403+
404+
Delete a content property from a Confluence page by its property ID.
405+
406+
#### Input
407+
408+
| Parameter | Type | Required | Description |
409+
| --------- | ---- | -------- | ----------- |
410+
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
411+
| `pageId` | string | Yes | The ID of the page containing the property |
412+
| `propertyId` | string | Yes | The ID of the property to delete |
413+
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
414+
415+
#### Output
416+
417+
| Parameter | Type | Description |
418+
| --------- | ---- | ----------- |
419+
| `ts` | string | ISO 8601 timestamp of the operation |
420+
| `pageId` | string | ID of the page |
421+
| `propertyId` | string | ID of the deleted property |
422+
| `deleted` | boolean | Deletion status |
423+
402424
### `confluence_search`
403425

404426
Search for content across Confluence pages, blog posts, and other content.
@@ -872,6 +894,90 @@ Add a label to a Confluence page for organization and categorization.
872894
| `labelName` | string | Name of the added label |
873895
| `labelId` | string | ID of the added label |
874896

897+
### `confluence_delete_label`
898+
899+
Remove a label from a Confluence page.
900+
901+
#### Input
902+
903+
| Parameter | Type | Required | Description |
904+
| --------- | ---- | -------- | ----------- |
905+
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
906+
| `pageId` | string | Yes | Confluence page ID to remove the label from |
907+
| `labelName` | string | Yes | Name of the label to remove |
908+
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
909+
910+
#### Output
911+
912+
| Parameter | Type | Description |
913+
| --------- | ---- | ----------- |
914+
| `ts` | string | ISO 8601 timestamp of the operation |
915+
| `pageId` | string | Page ID the label was removed from |
916+
| `labelName` | string | Name of the removed label |
917+
| `deleted` | boolean | Deletion status |
918+
919+
### `confluence_get_pages_by_label`
920+
921+
Retrieve all pages that have a specific label applied.
922+
923+
#### Input
924+
925+
| Parameter | Type | Required | Description |
926+
| --------- | ---- | -------- | ----------- |
927+
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
928+
| `labelId` | string | Yes | The ID of the label to get pages for |
929+
| `limit` | number | No | Maximum number of pages to return \(default: 50, max: 250\) |
930+
| `cursor` | string | No | Pagination cursor from previous response |
931+
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
932+
933+
#### Output
934+
935+
| Parameter | Type | Description |
936+
| --------- | ---- | ----------- |
937+
| `ts` | string | ISO 8601 timestamp of the operation |
938+
| `labelId` | string | ID of the label |
939+
| `pages` | array | Array of pages with this label |
940+
|`id` | string | Unique page identifier |
941+
|`title` | string | Page title |
942+
|`status` | string | Page status \(e.g., current, archived, trashed, draft\) |
943+
|`spaceId` | string | ID of the space containing the page |
944+
|`parentId` | string | ID of the parent page \(null if top-level\) |
945+
|`authorId` | string | Account ID of the page author |
946+
|`createdAt` | string | ISO 8601 timestamp when the page was created |
947+
|`version` | object | Page version information |
948+
|`number` | number | Version number |
949+
|`message` | string | Version message |
950+
|`minorEdit` | boolean | Whether this is a minor edit |
951+
|`authorId` | string | Account ID of the version author |
952+
|`createdAt` | string | ISO 8601 timestamp of version creation |
953+
| `nextCursor` | string | Cursor for fetching the next page of results |
954+
955+
### `confluence_list_space_labels`
956+
957+
List all labels associated with a Confluence space.
958+
959+
#### Input
960+
961+
| Parameter | Type | Required | Description |
962+
| --------- | ---- | -------- | ----------- |
963+
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
964+
| `spaceId` | string | Yes | The ID of the Confluence space to list labels from |
965+
| `limit` | number | No | Maximum number of labels to return \(default: 25, max: 250\) |
966+
| `cursor` | string | No | Pagination cursor from previous response |
967+
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
968+
969+
#### Output
970+
971+
| Parameter | Type | Description |
972+
| --------- | ---- | ----------- |
973+
| `ts` | string | ISO 8601 timestamp of the operation |
974+
| `spaceId` | string | ID of the space |
975+
| `labels` | array | Array of labels on the space |
976+
|`id` | string | Unique label identifier |
977+
|`name` | string | Label name |
978+
|`prefix` | string | Label prefix/type \(e.g., global, my, team\) |
979+
| `nextCursor` | string | Cursor for fetching the next page of results |
980+
875981
### `confluence_get_space`
876982

877983
Get details about a specific Confluence space.

apps/sim/app/api/tools/confluence/labels/route.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,84 @@ export async function GET(request: NextRequest) {
191191
)
192192
}
193193
}
194+
195+
// Delete a label from a page
196+
export async function DELETE(request: NextRequest) {
197+
try {
198+
const auth = await checkSessionOrInternalAuth(request)
199+
if (!auth.success || !auth.userId) {
200+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
201+
}
202+
203+
const {
204+
domain,
205+
accessToken,
206+
cloudId: providedCloudId,
207+
pageId,
208+
labelName,
209+
} = await request.json()
210+
211+
if (!domain) {
212+
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
213+
}
214+
215+
if (!accessToken) {
216+
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
217+
}
218+
219+
if (!pageId) {
220+
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
221+
}
222+
223+
if (!labelName) {
224+
return NextResponse.json({ error: 'Label name is required' }, { status: 400 })
225+
}
226+
227+
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
228+
if (!pageIdValidation.isValid) {
229+
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
230+
}
231+
232+
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
233+
234+
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
235+
if (!cloudIdValidation.isValid) {
236+
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
237+
}
238+
239+
const encodedLabel = encodeURIComponent(labelName.trim())
240+
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/content/${pageId}/label/${encodedLabel}`
241+
242+
const response = await fetch(url, {
243+
method: 'DELETE',
244+
headers: {
245+
Accept: 'application/json',
246+
Authorization: `Bearer ${accessToken}`,
247+
},
248+
})
249+
250+
if (!response.ok) {
251+
const errorData = await response.json().catch(() => null)
252+
logger.error('Confluence API error response:', {
253+
status: response.status,
254+
statusText: response.statusText,
255+
error: JSON.stringify(errorData, null, 2),
256+
})
257+
const errorMessage =
258+
errorData?.message || `Failed to delete Confluence label (${response.status})`
259+
return NextResponse.json({ error: errorMessage }, { status: response.status })
260+
}
261+
262+
return NextResponse.json({
263+
pageId,
264+
labelName,
265+
deleted: true,
266+
})
267+
} catch (error) {
268+
logger.error('Error deleting Confluence label:', error)
269+
return NextResponse.json(
270+
{ error: (error as Error).message || 'Internal server error' },
271+
{ status: 500 }
272+
)
273+
}
274+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
4+
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
5+
import { getConfluenceCloudId } from '@/tools/confluence/utils'
6+
7+
const logger = createLogger('ConfluencePagesByLabelAPI')
8+
9+
export const dynamic = 'force-dynamic'
10+
11+
export async function GET(request: NextRequest) {
12+
try {
13+
const auth = await checkSessionOrInternalAuth(request)
14+
if (!auth.success || !auth.userId) {
15+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
16+
}
17+
18+
const { searchParams } = new URL(request.url)
19+
const domain = searchParams.get('domain')
20+
const accessToken = searchParams.get('accessToken')
21+
const labelId = searchParams.get('labelId')
22+
const providedCloudId = searchParams.get('cloudId')
23+
const limit = searchParams.get('limit') || '50'
24+
const cursor = searchParams.get('cursor')
25+
26+
if (!domain) {
27+
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
28+
}
29+
30+
if (!accessToken) {
31+
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
32+
}
33+
34+
if (!labelId) {
35+
return NextResponse.json({ error: 'Label ID is required' }, { status: 400 })
36+
}
37+
38+
const labelIdValidation = validateAlphanumericId(labelId, 'labelId', 255)
39+
if (!labelIdValidation.isValid) {
40+
return NextResponse.json({ error: labelIdValidation.error }, { status: 400 })
41+
}
42+
43+
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
44+
45+
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
46+
if (!cloudIdValidation.isValid) {
47+
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
48+
}
49+
50+
const queryParams = new URLSearchParams()
51+
queryParams.append('limit', String(Math.min(Number(limit), 250)))
52+
if (cursor) {
53+
queryParams.append('cursor', cursor)
54+
}
55+
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/labels/${labelId}/pages?${queryParams.toString()}`
56+
57+
const response = await fetch(url, {
58+
method: 'GET',
59+
headers: {
60+
Accept: 'application/json',
61+
Authorization: `Bearer ${accessToken}`,
62+
},
63+
})
64+
65+
if (!response.ok) {
66+
const errorData = await response.json().catch(() => null)
67+
logger.error('Confluence API error response:', {
68+
status: response.status,
69+
statusText: response.statusText,
70+
error: JSON.stringify(errorData, null, 2),
71+
})
72+
const errorMessage = errorData?.message || `Failed to get pages by label (${response.status})`
73+
return NextResponse.json({ error: errorMessage }, { status: response.status })
74+
}
75+
76+
const data = await response.json()
77+
78+
const pages = (data.results || []).map((page: any) => ({
79+
id: page.id,
80+
title: page.title,
81+
status: page.status ?? null,
82+
spaceId: page.spaceId ?? null,
83+
parentId: page.parentId ?? null,
84+
authorId: page.authorId ?? null,
85+
createdAt: page.createdAt ?? null,
86+
version: page.version ?? null,
87+
}))
88+
89+
return NextResponse.json({
90+
pages,
91+
labelId,
92+
nextCursor: data._links?.next
93+
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
94+
: null,
95+
})
96+
} catch (error) {
97+
logger.error('Error getting pages by label:', error)
98+
return NextResponse.json(
99+
{ error: (error as Error).message || 'Internal server error' },
100+
{ status: 500 }
101+
)
102+
}
103+
}

0 commit comments

Comments
 (0)