import { GraphQLClient } from 'graphql-request'; import { getSdk, Sdk, PageTreeMode } from './generated/graphql.js'; const REQUEST_TIMEOUT_MS = 30000; export class WikiJSClient { private sdk: Sdk; private baseUrl: string; private defaultLocale: string; constructor(endpoint: string, authToken: string, defaultLocale: string = 'en') { const defaultHeaders = { Authorization: `Bearer ${authToken}` }; // Remove trailing slash if present this.baseUrl = endpoint.replace(/\/+$/, ''); this.defaultLocale = defaultLocale || 'en'; const endpointWithGraphql = `${this.baseUrl}/graphql`; const client = new GraphQLClient(endpointWithGraphql, { headers: defaultHeaders }); this.sdk = getSdk(client); } private signal(): AbortSignal { return AbortSignal.timeout(REQUEST_TIMEOUT_MS); } private buildUrl(path: string, locale?: string | null): string { const pathWithoutLeadingSlash = (path || '').replace(/^\/+/, ''); const encodedPath = pathWithoutLeadingSlash .split('/') .filter(Boolean) .map((seg) => encodeURIComponent(seg)) .join('/'); const loc = (locale || this.defaultLocale).trim(); // Wiki.js omits the locale segment only for the site's default locale. if (loc && loc !== this.defaultLocale) { return `${this.baseUrl}/${encodeURIComponent(loc)}/${encodedPath}`; } return `${this.baseUrl}/${encodedPath}`; } private addUrlToPage(page: T | null): (T & { url: string }) | null { if (!page) return null; const url = this.buildUrl(page.path || '', page.locale); return { ...page, url }; } private addUrlToPages(pages: T[]): (T & { url: string })[] { return pages.map((page) => ({ ...page, url: this.buildUrl(page.path || '', page.locale) })); } async searchPages(query: string, path?: string, locale?: string) { const result = await this.sdk.SearchPages({ query, path, locale }, undefined, this.signal()); const search = result.pages?.search; if (!search) { return { results: [], suggestions: [], totalHits: 0 }; } const filteredResults = (search.results || []).filter((r): r is NonNullable => r !== null); const withUrls = this.addUrlToPages(filteredResults); return { results: withUrls, suggestions: search.suggestions || [], totalHits: search.totalHits || 0 }; } async getAllPages({ limit, locale, tags }: { limit?: number; locale?: string; tags?: string[] } = {}) { const result = await this.sdk.GetAllPages({ limit, locale, tags }, undefined, this.signal()); const pages = result.pages?.list || []; return this.addUrlToPages(pages); } async getPageById(id: number) { const result = await this.sdk.GetPageById({ id }, undefined, this.signal()); const page = result.pages?.single || null; const withUrl = this.addUrlToPage(page); if (!withUrl) return null; return withUrl; } async getPageByPath(path: string, locale: string) { const result = await this.sdk.GetPageByPath({ path, locale }, undefined, this.signal()); const page = result.pages?.singleByPath || null; const withUrl = this.addUrlToPage(page); if (!withUrl) return null; return withUrl; } async createPage(params: { title: string; content: string; description: string; path: string; editor?: string; isPublished?: boolean; isPrivate?: boolean; locale?: string; tags?: string[]; }) { const result = await this.sdk.CreatePage( { title: params.title, content: params.content, description: params.description, path: params.path, editor: params.editor || 'markdown', isPublished: params.isPublished ?? true, isPrivate: params.isPrivate ?? false, locale: params.locale || 'en', tags: params.tags || [] }, undefined, this.signal() ); const created = result.pages?.create; if (!created) return null; const page = created.page ? this.addUrlToPage(created.page) : null; return { responseResult: created.responseResult, page }; } async updatePage(params: { id: number; title?: string; content?: string; description?: string; editor?: string; isPublished?: boolean; isPrivate?: boolean; locale?: string; tags?: string[]; path?: string; }) { // Fetch existing page to avoid clobbering fields the caller did not supply. let existing: Record | null = null; try { existing = (await this.getPageById(params.id)) as Record | null; } catch { existing = null; } const tagsFromExisting: string[] = Array.isArray(existing?.tags) && existing.tags.length && typeof existing.tags[0] === 'object' ? (existing.tags as Array<{ tag?: string }>).map((t) => t.tag).filter((t): t is string => Boolean(t)) : Array.isArray(existing?.tags) ? (existing.tags as unknown[]).filter((t): t is string => typeof t === 'string') : []; const merged = { id: params.id, title: params.title ?? (existing?.title as string | undefined) ?? '', content: params.content ?? (existing?.content as string | undefined) ?? '', description: params.description ?? (existing?.description as string | undefined) ?? '', editor: params.editor ?? (existing?.editor as string | undefined) ?? 'markdown', isPublished: params.isPublished ?? (existing?.isPublished as boolean | undefined) ?? true, isPrivate: params.isPrivate ?? (existing?.isPrivate as boolean | undefined) ?? false, locale: params.locale ?? (existing?.locale as string | undefined) ?? this.defaultLocale, tags: params.tags ?? tagsFromExisting, path: params.path ?? (existing?.path as string | undefined) ?? '' }; if (!merged.content || merged.content.trim().length === 0) { throw new Error('content is required to update a page'); } const result = await this.sdk.UpdatePage(merged, undefined, this.signal()); const updated = result.pages?.update; if (!updated) return null; const page = updated.page ? this.addUrlToPage(updated.page) : null; return { responseResult: updated.responseResult, page }; } async getPageHistory(id: number, offsetPage?: number, offsetSize?: number) { const result = await this.sdk.GetPageHistory({ id, offsetPage, offsetSize }, undefined, this.signal()); return result.pages?.history || null; } async getPageVersion(pageId: number, versionId: number) { const result = await this.sdk.GetPageVersion({ pageId, versionId }, undefined, this.signal()); return result.pages?.version || null; } async getPageTree(params: { path?: string; parent?: number; mode: 'FOLDERS' | 'PAGES' | 'ALL'; locale: string; includeAncestors?: boolean }) { const mode = (PageTreeMode as Record)[params.mode.charAt(0) + params.mode.slice(1).toLowerCase()] ?? PageTreeMode.Pages; const result = await this.sdk.GetPageTree({ ...params, mode }, undefined, this.signal()); return result.pages?.tree || null; } async listTags() { const result = await this.sdk.ListTags(undefined, undefined, this.signal()); return result.pages?.tags || []; } async searchTags(query: string) { const result = await this.sdk.SearchTags({ query }, undefined, this.signal()); return result.pages?.searchTags || []; } async getPageLinks(locale: string) { const result = await this.sdk.GetPageLinks({ locale }, undefined, this.signal()); return result.pages?.links || null; } async checkConflicts(id: number, checkoutDate: string) { const result = await this.sdk.CheckConflicts({ id, checkoutDate }, undefined, this.signal()); return result.pages?.checkConflicts ?? null; } async movePage(id: number, destinationPath: string, destinationLocale: string) { const result = await this.sdk.MovePage({ id, destinationPath, destinationLocale }, undefined, this.signal()); return result.pages?.move || null; } async deletePage(id: number) { const result = await this.sdk.DeletePage({ id }, undefined, this.signal()); return result.pages?.delete || null; } async restorePageVersion(pageId: number, versionId: number) { const result = await this.sdk.RestorePageVersion({ pageId, versionId }, undefined, this.signal()); return result.pages?.restore || null; } async convertPageEditor(id: number, editor: string) { const result = await this.sdk.ConvertPageEditor({ id, editor }, undefined, this.signal()); return result.pages?.convert || null; } async renderPage(id: number) { const result = await this.sdk.RenderPage({ id }, undefined, this.signal()); return result.pages?.render || null; } }