293 lines
7.9 KiB
TypeScript
293 lines
7.9 KiB
TypeScript
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<string, unknown>, excerpt?: string) {
|
|
const links = (page._links ?? {}) as Record<string, unknown>;
|
|
|
|
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<string, unknown>;
|
|
}
|
|
|
|
return {
|
|
async searchPages(input: SearchInput): Promise<CommandOutput<unknown>> {
|
|
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<string, unknown>;
|
|
|
|
const results = Array.isArray(raw.results) ? raw.results : [];
|
|
|
|
return {
|
|
ok: true,
|
|
data: {
|
|
pages: results.map((entry) => {
|
|
const result = entry as Record<string, unknown>;
|
|
return normalizePage(
|
|
config.baseUrl,
|
|
(result.content ?? {}) as Record<string, unknown>,
|
|
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<CommandOutput<unknown>> {
|
|
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<string, unknown>;
|
|
|
|
const body = ((raw.body ?? {}) as Record<string, unknown>).storage as Record<string, unknown> | undefined;
|
|
|
|
return {
|
|
ok: true,
|
|
data: {
|
|
page: {
|
|
...normalizePage(config.baseUrl, raw),
|
|
version: Number((((raw.version ?? {}) as Record<string, unknown>).number ?? 0)),
|
|
body: body?.value ? String(body.value) : "",
|
|
},
|
|
},
|
|
raw,
|
|
};
|
|
},
|
|
|
|
async listChildren(pageId: string, maxResults: number, startAt: number): Promise<CommandOutput<unknown>> {
|
|
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<string, unknown>;
|
|
|
|
const results = Array.isArray(raw.results) ? raw.results : [];
|
|
const links = (raw._links ?? {}) as Record<string, unknown>;
|
|
|
|
return {
|
|
ok: true,
|
|
data: {
|
|
pages: results.map((page) => normalizePage(config.baseUrl, page as Record<string, unknown>)),
|
|
nextCursor: links.next ? String(links.next) : null,
|
|
},
|
|
};
|
|
},
|
|
|
|
async createPage(input: CreateInput): Promise<CommandOutput<unknown>> {
|
|
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<CommandOutput<unknown>> {
|
|
const currentPage = await getPageForUpdate(input.pageId);
|
|
const version = (((currentPage.version ?? {}) as Record<string, unknown>).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<CommandOutput<unknown>> {
|
|
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,
|
|
};
|
|
},
|
|
};
|
|
}
|