import { sendJsonRequest } from "./http.js"; import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js"; type ConfluenceClientOptions = { config: AtlassianConfig; fetchImpl?: FetchLike; }; type SearchInput = { query: string; maxResults: number; startAt: number; }; type CreateInput = { space?: string; title: string; body: string; dryRun?: boolean; }; type UpdateInput = { pageId: string; title: string; body: string; dryRun?: boolean; }; type CommentInput = { pageId: string; body: string; dryRun?: boolean; }; type PageSummary = { id: string; title: string; type: string; status?: string; spaceId?: string; url?: string; }; function buildUrl(baseUrl: string, path: string) { return new URL(path, `${baseUrl}/`).toString(); } function normalizePage(baseUrl: string, page: Record, excerpt?: string) { const links = (page._links ?? {}) as Record; return { id: String(page.id ?? ""), title: String(page.title ?? ""), type: String(page.type ?? "page"), ...(page.status ? { status: String(page.status) } : {}), ...(page.spaceId ? { spaceId: String(page.spaceId) } : {}), ...(excerpt ? { excerpt } : {}), ...(links.webui ? { url: `${baseUrl}${String(links.webui)}` } : {}), }; } export function createConfluenceClient(options: ConfluenceClientOptions) { const config = options.config; async function getPageForUpdate(pageId: string) { return (await sendJsonRequest({ config, fetchImpl: options.fetchImpl, url: buildUrl(config.confluenceBaseUrl, `/wiki/api/v2/pages/${pageId}?body-format=storage`), method: "GET", errorPrefix: "Confluence request failed", })) as Record; } return { async searchPages(input: SearchInput): Promise> { const url = new URL("/wiki/rest/api/search", `${config.confluenceBaseUrl}/`); url.searchParams.set("cql", input.query); url.searchParams.set("limit", String(input.maxResults)); url.searchParams.set("start", String(input.startAt)); const raw = (await sendJsonRequest({ config, fetchImpl: options.fetchImpl, url: url.toString(), method: "GET", errorPrefix: "Confluence request failed", })) as Record; const results = Array.isArray(raw.results) ? raw.results : []; return { ok: true, data: { pages: results.map((entry) => { const result = entry as Record; return normalizePage( config.baseUrl, (result.content ?? {}) as Record, result.excerpt ? String(result.excerpt) : undefined, ); }), startAt: Number(raw.start ?? input.startAt), maxResults: Number(raw.limit ?? input.maxResults), total: Number(raw.totalSize ?? raw.size ?? results.length), }, }; }, async getPage(pageId: string): Promise> { const raw = (await sendJsonRequest({ config, fetchImpl: options.fetchImpl, url: buildUrl(config.confluenceBaseUrl, `/wiki/api/v2/pages/${pageId}?body-format=storage`), method: "GET", errorPrefix: "Confluence request failed", })) as Record; const body = ((raw.body ?? {}) as Record).storage as Record | undefined; return { ok: true, data: { page: { ...normalizePage(config.baseUrl, raw), version: Number((((raw.version ?? {}) as Record).number ?? 0)), body: body?.value ? String(body.value) : "", }, }, raw, }; }, async listChildren(pageId: string, maxResults: number, startAt: number): Promise> { const url = new URL(`/wiki/api/v2/pages/${pageId}/direct-children`, `${config.confluenceBaseUrl}/`); url.searchParams.set("limit", String(maxResults)); url.searchParams.set("cursor", String(startAt)); const raw = (await sendJsonRequest({ config, fetchImpl: options.fetchImpl, url: url.toString(), method: "GET", errorPrefix: "Confluence request failed", })) as Record; const results = Array.isArray(raw.results) ? raw.results : []; const links = (raw._links ?? {}) as Record; return { ok: true, data: { pages: results.map((page) => normalizePage(config.baseUrl, page as Record)), nextCursor: links.next ? String(links.next) : null, }, }; }, async createPage(input: CreateInput): Promise> { const spaceId = input.space || config.defaultSpace; if (!spaceId) { throw new Error("conf-create requires --space or ATLASSIAN_DEFAULT_SPACE"); } const request = { method: "POST" as const, url: buildUrl(config.confluenceBaseUrl, "/wiki/api/v2/pages"), body: { spaceId, title: input.title, status: "current", body: { representation: "storage", value: input.body, }, }, }; if (input.dryRun) { return { ok: true, dryRun: true, data: request, }; } const raw = await sendJsonRequest({ config, fetchImpl: options.fetchImpl, url: request.url, method: request.method, body: request.body, errorPrefix: "Confluence request failed", }); return { ok: true, data: raw, }; }, async updatePage(input: UpdateInput): Promise> { const currentPage = await getPageForUpdate(input.pageId); const version = (((currentPage.version ?? {}) as Record).number ?? 0) as number; const spaceId = String(currentPage.spaceId ?? ""); const request = { method: "PUT" as const, url: buildUrl(config.confluenceBaseUrl, `/wiki/api/v2/pages/${input.pageId}`), body: { id: input.pageId, status: String(currentPage.status ?? "current"), title: input.title, spaceId, version: { number: Number(version) + 1, }, body: { representation: "storage", value: input.body, }, }, }; if (input.dryRun) { return { ok: true, dryRun: true, data: request, }; } const raw = await sendJsonRequest({ config, fetchImpl: options.fetchImpl, url: request.url, method: request.method, body: request.body, errorPrefix: "Confluence request failed", handleResponseError(response) { if (response.status === 409) { return new Error(`Confluence update conflict: page ${input.pageId} was updated by someone else`); } return undefined; }, }); return { ok: true, data: raw, }; }, async commentPage(input: CommentInput): Promise> { const request = { method: "POST" as const, url: buildUrl(config.confluenceBaseUrl, "/wiki/api/v2/footer-comments"), body: { pageId: input.pageId, body: { representation: "storage", value: input.body, }, }, }; if (input.dryRun) { return { ok: true, dryRun: true, data: request, }; } const raw = await sendJsonRequest({ config, fetchImpl: options.fetchImpl, url: request.url, method: request.method, body: request.body, errorPrefix: "Confluence request failed", }); return { ok: true, data: raw, }; }, }; }