Skip to content

Commit 58fb25a

Browse files
committed
fix(connectors): paginate the Confluence pages selector instead of returning one page
`/api/tools/confluence/pages` issued a single request and discarded the `_links.next` cursor Confluence returns, so the picker showed only the first `limit` pages of however many exist — a search for a real page found nothing, with no error and no truncation signal. Threads the documented opaque cursor and moves the selector to `fetchPage`, so the option list drains like `confluence.spaces` and reports real `hasMore` / `truncated` state. The `title` server-side filter is unchanged and now paginates too. `limit` is deliberately left at its existing default: the endpoint's maximum is not confirmable from Atlassian's published reference, and raising it is not needed for correctness. Also guards `data.results`, which threw when the response omitted it.
1 parent 3d779e9 commit 58fb25a

3 files changed

Lines changed: 42 additions & 8 deletions

File tree

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
2323
const parsed = await parseRequest(confluencePagesSelectorContract, request, {})
2424
if (!parsed.success) return parsed.response
2525

26-
const { domain, accessToken, title, cloudId: providedCloudId, limit } = parsed.data.body
26+
const { domain, accessToken, title, cloudId: providedCloudId, limit, cursor } = parsed.data.body
2727

2828
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
2929

@@ -43,6 +43,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4343
queryParams.append('title', title)
4444
}
4545

46+
if (cursor) {
47+
queryParams.append('cursor', cursor)
48+
}
49+
4650
const queryString = queryParams.toString()
4751
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl
4852

@@ -82,8 +86,24 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
8286
}
8387
}
8488

89+
/**
90+
* Confluence paginates with an opaque cursor carried in `_links.next`. Reading
91+
* it is what lets the selector continue past the first page — without it the
92+
* dropdown silently showed only `limit` pages of however many exist.
93+
*/
94+
let nextCursor: string | undefined
95+
const nextLink = data._links?.next as string | undefined
96+
if (nextLink) {
97+
try {
98+
nextCursor =
99+
new URL(nextLink, 'https://placeholder').searchParams.get('cursor') || undefined
100+
} catch {
101+
nextCursor = undefined
102+
}
103+
}
104+
85105
return NextResponse.json({
86-
files: data.results.map((page: any) => ({
106+
files: (data.results || []).map((page: any) => ({
87107
id: page.id,
88108
name: page.title,
89109
mimeType: 'confluence/page',
@@ -92,6 +112,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
92112
spaceId: page.spaceId,
93113
webViewLink: page._links?.webui || '',
94114
})),
115+
nextCursor,
95116
})
96117
} catch (error) {
97118
logger.error('Error fetching Confluence pages:', error)

apps/sim/hooks/selectors/providers/confluence/selectors.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,14 @@ export const confluenceSelectors = {
8888
search ?? '',
8989
],
9090
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
91-
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
91+
/**
92+
* Paged rather than a single fetch: `/pages` is cursor-paginated, so one request
93+
* returned only the first `limit` pages of however many exist and the rest were
94+
* unreachable — a search for a real page silently found nothing. `search` is
95+
* still forwarded as the server-side `title` filter, and pagination now applies
96+
* to the filtered stream too.
97+
*/
98+
fetchPage: async ({ context, search, cursor, signal }) => {
9299
const credentialId = ensureCredential(context, 'confluence.pages')
93100
const domain = ensureDomain(context, 'confluence.pages')
94101
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
@@ -101,13 +108,17 @@ export const confluenceSelectors = {
101108
accessToken: bundle.accessToken,
102109
cloudId: bundle.cloudId,
103110
title: search,
111+
cursor,
104112
},
105113
signal,
106114
})
107-
return (data.files || []).map((file) => ({
108-
id: file.id,
109-
label: file.name,
110-
}))
115+
return {
116+
items: (data.files || []).map((file) => ({
117+
id: file.id,
118+
label: file.name,
119+
})),
120+
nextCursor: data.nextCursor,
121+
}
111122
},
112123
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
113124
if (!detailId) return null

apps/sim/lib/api/contracts/selectors/confluence.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export const confluencePagesBodySchema = z.object({
2424
cloudId: optionalString,
2525
title: optionalString,
2626
limit: z.number().int().positive().optional().default(50),
27+
/** Opaque Confluence cursor for the next page, echoed back from `nextCursor`. */
28+
cursor: optionalString,
2729
})
2830

2931
/**
@@ -386,7 +388,7 @@ export const confluenceSpacesSelectorContract = definePostSelector(
386388
export const confluencePagesSelectorContract = definePostSelector(
387389
'/api/tools/confluence/pages',
388390
confluencePagesBodySchema,
389-
z.object({ files: z.array(fileOptionSchema) })
391+
z.object({ files: z.array(fileOptionSchema), nextCursor: optionalString })
390392
)
391393

392394
export const confluencePageSelectorContract = definePostSelector(

0 commit comments

Comments
 (0)