/** * Pure REST helpers for the corporate wiki (Confluence Server/DC, rest/api). * `docsUrl` comes from config/company.json; `CONFLUENCE_TOKEN` from the env. * No pi API — unit-testable. */ import { apiRequest, requireToken, ApiError, resolveBaseUrl } from "./atlassian.ts"; import { readLocalConfig } from "./company-config.ts"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const CONFIG_PATH = path.join(HERE, "..", "..", "config", "company.json"); /** Resolve the Confluence base URL from company.json (env override for labs). */ export function confluenceBaseUrl(): string { const cfg = readLocalConfig(CONFIG_PATH); const url = resolveBaseUrl("CONFLUENCE_URL", cfg.docsUrl); if (!url) throw new ApiError("Не задан docsUrl в config/company.json и нет CONFLUENCE_URL."); return url; } function token(): string { return requireToken( "CONFLUENCE_TOKEN", "personal access token Confluence. Где брать: Профиль → Security → API tokens / Personal Access Tokens → создать", ); } export interface Page { id: string; title: string; url?: string; body?: string; spaceKey?: string; } export interface SearchResult { pages: Page[]; total: number; } /** CQL search for Confluence pages. `cql` supports the standard text/title/space operators. */ export async function search(cql: string, opts: { limit?: number; expandBody?: boolean } = {}): Promise { const base = confluenceBaseUrl(); if (!cql) throw new ApiError("search требует CQL-запрос (например 'text=\"foo\"')."); const data: any = await apiRequest(base, token(), "/rest/api/content/search", { query: { cql, limit: opts.limit ?? 25, expand: opts.expandBody ? "body.view" : undefined }, }); const pages: Page[] = (data?.results ?? []).map((p: any) => ({ id: p.id, title: p.title ?? "", spaceKey: p.space?.key, body: opts.expandBody ? (p.body?.view?.value ?? "") : undefined, url: `${base}/spaces/${p.space?.key ?? "~self"}/pages/${p.id}`, })); return { pages, total: data?.size ?? pages.length }; } export async function pageGet(id: string, opts: { expandBody?: boolean } = {}): Promise { const base = confluenceBaseUrl(); if (!id) throw new ApiError("pageGet требует id страницы."); const data: any = await apiRequest(base, token(), `/rest/api/content/${encodeURIComponent(id)}`, { query: opts.expandBody ? { expand: "body.view,version" } : undefined, }); return { id: data?.id ?? id, title: data?.title ?? "", spaceKey: data?.space?.key, body: opts.expandBody ? (data?.body?.view?.value ?? "") : undefined, url: `${base}/spaces/${data?.space?.key ?? "~self"}/pages/${data?.id ?? id}`, }; } export interface PageCreateInput { spaceKey: string; title: string; body: string; // HTML (Storage format) or text parentId?: string; representation?: "storage" | "editor" | "wiki"; } /** Create a new Confluence page. `body` is HTML in Storage format by default. */ export async function pageCreate(input: PageCreateInput): Promise { const base = confluenceBaseUrl(); if (!input.spaceKey || !input.title || !input.body) { throw new ApiError("pageCreate требует spaceKey, title и body."); } const rep = input.representation ?? "storage"; const body: Record = { type: "page", title: input.title, space: { key: input.spaceKey }, body: { [rep]: { value: input.body, representation: rep } }, }; if (input.parentId) (body as any).ancestors = [{ id: input.parentId }]; const data: any = await apiRequest(base, token(), "/rest/api/content", { method: "POST", body }); return { id: data?.id ?? "", title: data?.title ?? input.title, spaceKey: data?.space?.key ?? input.spaceKey, url: `${base}/spaces/${input.spaceKey}/pages/${data?.id ?? ""}`, }; }