index.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import { GraphQLClient } from 'graphql-request';
  2. import { getSdk, Sdk, PageTreeMode } from './generated/graphql.js';
  3. const REQUEST_TIMEOUT_MS = 30000;
  4. export class WikiJSClient {
  5. private sdk: Sdk;
  6. private baseUrl: string;
  7. private defaultLocale: string;
  8. constructor(endpoint: string, authToken: string, defaultLocale: string = 'en') {
  9. const defaultHeaders = {
  10. Authorization: `Bearer ${authToken}`
  11. };
  12. // Remove trailing slash if present
  13. this.baseUrl = endpoint.replace(/\/+$/, '');
  14. this.defaultLocale = defaultLocale || 'en';
  15. const endpointWithGraphql = `${this.baseUrl}/graphql`;
  16. const client = new GraphQLClient(endpointWithGraphql, {
  17. headers: defaultHeaders
  18. });
  19. this.sdk = getSdk(client);
  20. }
  21. private signal(): AbortSignal {
  22. return AbortSignal.timeout(REQUEST_TIMEOUT_MS);
  23. }
  24. private buildUrl(path: string, locale?: string | null): string {
  25. const pathWithoutLeadingSlash = (path || '').replace(/^\/+/, '');
  26. const encodedPath = pathWithoutLeadingSlash
  27. .split('/')
  28. .filter(Boolean)
  29. .map((seg) => encodeURIComponent(seg))
  30. .join('/');
  31. const loc = (locale || this.defaultLocale).trim();
  32. // Wiki.js omits the locale segment only for the site's default locale.
  33. if (loc && loc !== this.defaultLocale) {
  34. return `${this.baseUrl}/${encodeURIComponent(loc)}/${encodedPath}`;
  35. }
  36. return `${this.baseUrl}/${encodedPath}`;
  37. }
  38. private addUrlToPage<T extends { path?: string | null; locale?: string | null }>(page: T | null): (T & { url: string }) | null {
  39. if (!page) return null;
  40. const url = this.buildUrl(page.path || '', page.locale);
  41. return { ...page, url };
  42. }
  43. private addUrlToPages<T extends { path?: string | null; locale?: string | null }>(pages: T[]): (T & { url: string })[] {
  44. return pages.map((page) => ({
  45. ...page,
  46. url: this.buildUrl(page.path || '', page.locale)
  47. }));
  48. }
  49. async searchPages(query: string, path?: string, locale?: string) {
  50. const result = await this.sdk.SearchPages({ query, path, locale }, undefined, this.signal());
  51. const search = result.pages?.search;
  52. if (!search) {
  53. return { results: [], suggestions: [], totalHits: 0 };
  54. }
  55. const filteredResults = (search.results || []).filter((r): r is NonNullable<typeof r> => r !== null);
  56. const withUrls = this.addUrlToPages(filteredResults);
  57. return {
  58. results: withUrls,
  59. suggestions: search.suggestions || [],
  60. totalHits: search.totalHits || 0
  61. };
  62. }
  63. async getAllPages({ limit, locale, tags }: { limit?: number; locale?: string; tags?: string[] } = {}) {
  64. const result = await this.sdk.GetAllPages({ limit, locale, tags }, undefined, this.signal());
  65. const pages = result.pages?.list || [];
  66. return this.addUrlToPages(pages);
  67. }
  68. async getPageById(id: number) {
  69. const result = await this.sdk.GetPageById({ id }, undefined, this.signal());
  70. const page = result.pages?.single || null;
  71. const withUrl = this.addUrlToPage(page);
  72. if (!withUrl) return null;
  73. return withUrl;
  74. }
  75. async getPageByPath(path: string, locale: string) {
  76. const result = await this.sdk.GetPageByPath({ path, locale }, undefined, this.signal());
  77. const page = result.pages?.singleByPath || null;
  78. const withUrl = this.addUrlToPage(page);
  79. if (!withUrl) return null;
  80. return withUrl;
  81. }
  82. async createPage(params: {
  83. title: string;
  84. content: string;
  85. description: string;
  86. path: string;
  87. editor?: string;
  88. isPublished?: boolean;
  89. isPrivate?: boolean;
  90. locale?: string;
  91. tags?: string[];
  92. }) {
  93. const result = await this.sdk.CreatePage(
  94. {
  95. title: params.title,
  96. content: params.content,
  97. description: params.description,
  98. path: params.path,
  99. editor: params.editor || 'markdown',
  100. isPublished: params.isPublished ?? true,
  101. isPrivate: params.isPrivate ?? false,
  102. locale: params.locale || 'en',
  103. tags: params.tags || []
  104. },
  105. undefined,
  106. this.signal()
  107. );
  108. const created = result.pages?.create;
  109. if (!created) return null;
  110. const page = created.page ? this.addUrlToPage(created.page) : null;
  111. return {
  112. responseResult: created.responseResult,
  113. page
  114. };
  115. }
  116. async updatePage(params: {
  117. id: number;
  118. title?: string;
  119. content?: string;
  120. description?: string;
  121. editor?: string;
  122. isPublished?: boolean;
  123. isPrivate?: boolean;
  124. locale?: string;
  125. tags?: string[];
  126. path?: string;
  127. }) {
  128. // Fetch existing page to avoid clobbering fields the caller did not supply.
  129. let existing: Record<string, unknown> | null = null;
  130. try {
  131. existing = (await this.getPageById(params.id)) as Record<string, unknown> | null;
  132. } catch {
  133. existing = null;
  134. }
  135. const tagsFromExisting: string[] =
  136. Array.isArray(existing?.tags) && existing.tags.length && typeof existing.tags[0] === 'object'
  137. ? (existing.tags as Array<{ tag?: string }>).map((t) => t.tag).filter((t): t is string => Boolean(t))
  138. : Array.isArray(existing?.tags)
  139. ? (existing.tags as unknown[]).filter((t): t is string => typeof t === 'string')
  140. : [];
  141. const merged = {
  142. id: params.id,
  143. title: params.title ?? (existing?.title as string | undefined) ?? '',
  144. content: params.content ?? (existing?.content as string | undefined) ?? '',
  145. description: params.description ?? (existing?.description as string | undefined) ?? '',
  146. editor: params.editor ?? (existing?.editor as string | undefined) ?? 'markdown',
  147. isPublished: params.isPublished ?? (existing?.isPublished as boolean | undefined) ?? true,
  148. isPrivate: params.isPrivate ?? (existing?.isPrivate as boolean | undefined) ?? false,
  149. locale: params.locale ?? (existing?.locale as string | undefined) ?? this.defaultLocale,
  150. tags: params.tags ?? tagsFromExisting,
  151. path: params.path ?? (existing?.path as string | undefined) ?? ''
  152. };
  153. if (!merged.content || merged.content.trim().length === 0) {
  154. throw new Error('content is required to update a page');
  155. }
  156. const result = await this.sdk.UpdatePage(merged, undefined, this.signal());
  157. const updated = result.pages?.update;
  158. if (!updated) return null;
  159. const page = updated.page ? this.addUrlToPage(updated.page) : null;
  160. return {
  161. responseResult: updated.responseResult,
  162. page
  163. };
  164. }
  165. async getPageHistory(id: number, offsetPage?: number, offsetSize?: number) {
  166. const result = await this.sdk.GetPageHistory({ id, offsetPage, offsetSize }, undefined, this.signal());
  167. return result.pages?.history || null;
  168. }
  169. async getPageVersion(pageId: number, versionId: number) {
  170. const result = await this.sdk.GetPageVersion({ pageId, versionId }, undefined, this.signal());
  171. return result.pages?.version || null;
  172. }
  173. async getPageTree(params: { path?: string; parent?: number; mode: 'FOLDERS' | 'PAGES' | 'ALL'; locale: string; includeAncestors?: boolean }) {
  174. const mode = (PageTreeMode as Record<string, PageTreeMode>)[params.mode.charAt(0) + params.mode.slice(1).toLowerCase()] ?? PageTreeMode.Pages;
  175. const result = await this.sdk.GetPageTree({ ...params, mode }, undefined, this.signal());
  176. return result.pages?.tree || null;
  177. }
  178. async listTags() {
  179. const result = await this.sdk.ListTags(undefined, undefined, this.signal());
  180. return result.pages?.tags || [];
  181. }
  182. async searchTags(query: string) {
  183. const result = await this.sdk.SearchTags({ query }, undefined, this.signal());
  184. return result.pages?.searchTags || [];
  185. }
  186. async getPageLinks(locale: string) {
  187. const result = await this.sdk.GetPageLinks({ locale }, undefined, this.signal());
  188. return result.pages?.links || null;
  189. }
  190. async checkConflicts(id: number, checkoutDate: string) {
  191. const result = await this.sdk.CheckConflicts({ id, checkoutDate }, undefined, this.signal());
  192. return result.pages?.checkConflicts ?? null;
  193. }
  194. async movePage(id: number, destinationPath: string, destinationLocale: string) {
  195. const result = await this.sdk.MovePage({ id, destinationPath, destinationLocale }, undefined, this.signal());
  196. return result.pages?.move || null;
  197. }
  198. async deletePage(id: number) {
  199. const result = await this.sdk.DeletePage({ id }, undefined, this.signal());
  200. return result.pages?.delete || null;
  201. }
  202. async restorePageVersion(pageId: number, versionId: number) {
  203. const result = await this.sdk.RestorePageVersion({ pageId, versionId }, undefined, this.signal());
  204. return result.pages?.restore || null;
  205. }
  206. async convertPageEditor(id: number, editor: string) {
  207. const result = await this.sdk.ConvertPageEditor({ id, editor }, undefined, this.signal());
  208. return result.pages?.convert || null;
  209. }
  210. async renderPage(id: number) {
  211. const result = await this.sdk.RenderPage({ id }, undefined, this.signal());
  212. return result.pages?.render || null;
  213. }
  214. }