Skip to content

Commit 6f4ee47

Browse files
committed
fix(confluence): exclude archived pages from KB connector listings so reconciliation purges them
1 parent 227cd65 commit 6f4ee47

2 files changed

Lines changed: 50 additions & 11 deletions

File tree

apps/sim/connectors/confluence/confluence.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @vitest-environment node
33
*/
44
import { describe, expect, it } from 'vitest'
5-
import { escapeCql } from '@/connectors/confluence/confluence'
5+
import { escapeCql, isCurrentContent } from '@/connectors/confluence/confluence'
66

77
describe('escapeCql', () => {
88
it.concurrent('returns plain strings unchanged', () => {
@@ -29,3 +29,22 @@ describe('escapeCql', () => {
2929
expect(escapeCql("it's a test & <tag>")).toBe("it's a test & <tag>")
3030
})
3131
})
32+
33+
describe('isCurrentContent', () => {
34+
it.concurrent('keeps current content', () => {
35+
expect(isCurrentContent({ id: '1', status: 'current' })).toBe(true)
36+
})
37+
38+
it.concurrent('keeps content with no status field', () => {
39+
expect(isCurrentContent({ id: '1' })).toBe(true)
40+
})
41+
42+
it.concurrent('excludes archived content', () => {
43+
expect(isCurrentContent({ id: '1', status: 'archived' })).toBe(false)
44+
})
45+
46+
it.concurrent('excludes trashed and deleted content', () => {
47+
expect(isCurrentContent({ id: '1', status: 'trashed' })).toBe(false)
48+
expect(isCurrentContent({ id: '1', status: 'deleted' })).toBe(false)
49+
})
50+
})

apps/sim/connectors/confluence/confluence.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ export function escapeCql(value: string): string {
1515
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
1616
}
1717

18+
/**
19+
* Keeps only content that is still current in Confluence. The v2
20+
* `/spaces/{id}/pages` endpoint includes `archived` pages by default and CQL has
21+
* no status filter, so without this guard archived pages stay in every listing,
22+
* keep getting upserted, and never fall out via deletion reconciliation (which
23+
* removes only documents absent from the listing). Items with no status field
24+
* are kept — only an explicit non-current status excludes a result.
25+
*/
26+
export function isCurrentContent(item: Record<string, unknown>): boolean {
27+
return item.status == null || item.status === 'current'
28+
}
29+
1830
/**
1931
* Builds a CQL clause restricting content to the given space keys.
2032
* Single key uses `space = "X"`; multiple keys use `space in ("X","Y")`.
@@ -272,7 +284,7 @@ export const confluenceConnector: ConnectorConfig = {
272284
}
273285
}
274286

275-
if (!page) return null
287+
if (!page || !isCurrentContent(page)) return null
276288
const body = page.body as Record<string, unknown> | undefined
277289
const view = body?.view as Record<string, unknown> | undefined
278290
const rawContent = (view?.value as string) || ''
@@ -381,6 +393,12 @@ async function listDocumentsV2(
381393
): Promise<ExternalDocumentList> {
382394
const queryParams = new URLSearchParams()
383395
queryParams.append('limit', '250')
396+
/**
397+
* Restrict to current content: the pages endpoint defaults to
398+
* `current,archived`, so archived pages would otherwise stay in the listing
399+
* forever and never be purged by deletion reconciliation.
400+
*/
401+
queryParams.append('status', 'current')
384402
if (cursor) {
385403
queryParams.append('cursor', cursor)
386404
}
@@ -410,13 +428,15 @@ async function listDocumentsV2(
410428
const data = await response.json()
411429
const results = data.results || []
412430

413-
const documents: ExternalDocument[] = results.map((page: Record<string, unknown>) => {
414-
const links = page._links as Record<string, string> | undefined
415-
return pageToStub(page, {
416-
spaceId: page.spaceId,
417-
sourceUrl: links?.webui ? `https://${domain}/wiki${links.webui}` : undefined,
431+
const documents: ExternalDocument[] = (results as Record<string, unknown>[])
432+
.filter(isCurrentContent)
433+
.map((page) => {
434+
const links = page._links as Record<string, string> | undefined
435+
return pageToStub(page, {
436+
spaceId: page.spaceId,
437+
sourceUrl: links?.webui ? `https://${domain}/wiki${links.webui}` : undefined,
438+
})
418439
})
419-
})
420440

421441
let nextCursor: string | undefined
422442
const nextLink = (data._links as Record<string, string>)?.next
@@ -594,9 +614,9 @@ async function listDocumentsViaCql(
594614
const data = await response.json()
595615
const results = data.results || []
596616

597-
const documents: ExternalDocument[] = results.map((item: Record<string, unknown>) =>
598-
cqlResultToStub(item, domain)
599-
)
617+
const documents: ExternalDocument[] = (results as Record<string, unknown>[])
618+
.filter(isCurrentContent)
619+
.map((item) => cqlResultToStub(item, domain))
600620

601621
const totalFetched = ((syncContext?.totalDocsFetched as number) ?? 0) + documents.length
602622
if (syncContext) syncContext.totalDocsFetched = totalFetched

0 commit comments

Comments
 (0)