import * as plugins from './bookstack.plugins.js'; import type { ITestConnectionResult, IBookStackListParams, IBookStackListResponse, IBookStackBook, IBookStackChapter, IBookStackPage, IBookStackShelf, IBookStackAttachment, IBookStackComment, IBookStackImage, IBookStackUser, IBookStackRole, IBookStackSearchResult, IBookStackAuditLogEntry, IBookStackRecycleBinItem, IBookStackContentPermission, IBookStackSystemInfo, IBookStackTag, } from './bookstack.interfaces.js'; import { BookStackBook } from './bookstack.classes.book.js'; import { BookStackChapter } from './bookstack.classes.chapter.js'; import { BookStackPage } from './bookstack.classes.page.js'; import { BookStackShelf } from './bookstack.classes.shelf.js'; import { autoPaginate } from './bookstack.helpers.js'; export class BookStackAccount { private baseUrl: string; private tokenId: string; private tokenSecret: string; constructor(baseUrl: string, tokenId: string, tokenSecret: string) { this.baseUrl = baseUrl.replace(/\/+$/, ''); this.tokenId = tokenId; this.tokenSecret = tokenSecret; } // =========================================================================== // HTTP helpers (internal) // =========================================================================== /** @internal */ async request( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, data?: any, ): Promise { const url = `${this.baseUrl}/api${path}`; let builder = plugins.smartrequest.SmartRequest.create() .url(url) .header('Authorization', `Token ${this.tokenId}:${this.tokenSecret}`) .header('Content-Type', 'application/json'); if (data) { builder = builder.json(data); } let response: Awaited>; switch (method) { case 'GET': response = await builder.get(); break; case 'POST': response = await builder.post(); break; case 'PUT': response = await builder.put(); break; case 'DELETE': response = await builder.delete(); break; } if (!response.ok) { const errorText = await response.text(); throw new Error(`${method} ${path}: ${response.status} ${response.statusText} - ${errorText}`); } try { return (await response.json()) as T; } catch { return undefined as unknown as T; } } /** @internal */ async requestText( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, ): Promise { const url = `${this.baseUrl}/api${path}`; let builder = plugins.smartrequest.SmartRequest.create() .url(url) .header('Authorization', `Token ${this.tokenId}:${this.tokenSecret}`) .header('Accept', 'text/plain'); let response: Awaited>; switch (method) { case 'GET': response = await builder.get(); break; case 'POST': response = await builder.post(); break; case 'PUT': response = await builder.put(); break; case 'DELETE': response = await builder.delete(); break; } if (!response.ok) { const errorText = await response.text(); throw new Error(`${method} ${path}: ${response.status} ${response.statusText} - ${errorText}`); } return response.text(); } /** @internal */ async requestBinary(path: string): Promise { const url = `${this.baseUrl}/api${path}`; const response = await fetch(url, { headers: { Authorization: `Token ${this.tokenId}:${this.tokenSecret}`, }, }); if (!response.ok) { throw new Error(`GET ${path}: ${response.status} ${response.statusText}`); } const buf = await response.arrayBuffer(); return new Uint8Array(buf); } /** @internal — build a URL with list query params */ buildListUrl(basePath: string, opts?: IBookStackListParams): string { const params: string[] = []; if (opts?.count !== undefined) params.push(`count=${opts.count}`); if (opts?.offset !== undefined) params.push(`offset=${opts.offset}`); if (opts?.sort) params.push(`sort=${encodeURIComponent(opts.sort)}`); if (opts?.filter) { for (const [key, value] of Object.entries(opts.filter)) { params.push(`filter[${encodeURIComponent(key)}]=${encodeURIComponent(value)}`); } } return params.length > 0 ? `${basePath}?${params.join('&')}` : basePath; } // =========================================================================== // Connection // =========================================================================== public async testConnection(): Promise { try { await this.request>('GET', '/books?count=1'); return { ok: true }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) }; } } // =========================================================================== // Books // =========================================================================== public async getBooks(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/books', { ...opts, offset, count }), ), opts, ).then((books) => books.map((b) => new BookStackBook(this, b))); } public async getBook(id: number): Promise { const raw = await this.request('GET', `/books/${id}`); return new BookStackBook(this, raw); } public async createBook(data: { name: string; description?: string; description_html?: string; tags?: IBookStackTag[]; default_template_id?: number | null; }): Promise { const raw = await this.request('POST', '/books', data); return new BookStackBook(this, raw); } // =========================================================================== // Chapters // =========================================================================== public async getChapters(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/chapters', { ...opts, offset, count }), ), opts, ).then((chapters) => chapters.map((c) => new BookStackChapter(this, c))); } public async getChapter(id: number): Promise { const raw = await this.request('GET', `/chapters/${id}`); return new BookStackChapter(this, raw); } public async createChapter(data: { book_id: number; name: string; description?: string; description_html?: string; tags?: IBookStackTag[]; priority?: number; default_template_id?: number | null; }): Promise { const raw = await this.request('POST', '/chapters', data); return new BookStackChapter(this, raw); } // =========================================================================== // Pages // =========================================================================== public async getPages(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/pages', { ...opts, offset, count }), ), opts, ).then((pages) => pages.map((p) => new BookStackPage(this, p))); } public async getPage(id: number): Promise { const raw = await this.request('GET', `/pages/${id}`); return new BookStackPage(this, raw); } public async createPage(data: { name: string; book_id?: number; chapter_id?: number; html?: string; markdown?: string; tags?: IBookStackTag[]; priority?: number; }): Promise { const raw = await this.request('POST', '/pages', data); return new BookStackPage(this, raw); } // =========================================================================== // Shelves // =========================================================================== public async getShelves(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/shelves', { ...opts, offset, count }), ), opts, ).then((shelves) => shelves.map((s) => new BookStackShelf(this, s))); } public async getShelf(id: number): Promise { const raw = await this.request('GET', `/shelves/${id}`); return new BookStackShelf(this, raw); } public async createShelf(data: { name: string; description?: string; description_html?: string; books?: number[]; tags?: IBookStackTag[]; }): Promise { const raw = await this.request('POST', '/shelves', data); return new BookStackShelf(this, raw); } // =========================================================================== // Attachments // =========================================================================== public async getAttachments(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/attachments', { ...opts, offset, count }), ), opts, ); } public async getAttachment(id: number): Promise { return this.request('GET', `/attachments/${id}`); } public async createAttachment(data: { name: string; uploaded_to: number; link?: string; }): Promise { return this.request('POST', '/attachments', data); } public async updateAttachment(id: number, data: { name?: string; uploaded_to?: number; link?: string; }): Promise { return this.request('PUT', `/attachments/${id}`, data); } public async deleteAttachment(id: number): Promise { await this.request('DELETE', `/attachments/${id}`); } // =========================================================================== // Comments // =========================================================================== public async getComments(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/comments', { ...opts, offset, count }), ), opts, ); } public async getComment(id: number): Promise { return this.request('GET', `/comments/${id}`); } public async createComment(data: { page_id: number; html: string; reply_to?: number; content_ref?: string; }): Promise { return this.request('POST', '/comments', data); } public async updateComment(id: number, data: { html?: string; archived?: boolean; }): Promise { return this.request('PUT', `/comments/${id}`, data); } public async deleteComment(id: number): Promise { await this.request('DELETE', `/comments/${id}`); } // =========================================================================== // Image Gallery // =========================================================================== public async getImages(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/image-gallery', { ...opts, offset, count }), ), opts, ); } public async getImage(id: number): Promise { return this.request('GET', `/image-gallery/${id}`); } public async updateImage(id: number, data: { name?: string; }): Promise { return this.request('PUT', `/image-gallery/${id}`, data); } public async deleteImage(id: number): Promise { await this.request('DELETE', `/image-gallery/${id}`); } // =========================================================================== // Users // =========================================================================== public async getUsers(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/users', { ...opts, offset, count }), ), opts, ); } public async getUser(id: number): Promise { return this.request('GET', `/users/${id}`); } public async createUser(data: { name: string; email: string; password?: string; roles?: number[]; send_invite?: boolean; external_auth_id?: string; language?: string; }): Promise { return this.request('POST', '/users', data); } public async updateUser(id: number, data: { name?: string; email?: string; password?: string; roles?: number[]; external_auth_id?: string; language?: string; }): Promise { return this.request('PUT', `/users/${id}`, data); } public async deleteUser(id: number, opts?: { migrate_ownership_id?: number }): Promise { const path = opts?.migrate_ownership_id ? `/users/${id}?migrate_ownership_id=${opts.migrate_ownership_id}` : `/users/${id}`; await this.request('DELETE', path); } // =========================================================================== // Roles // =========================================================================== public async getRoles(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/roles', { ...opts, offset, count }), ), opts, ); } public async getRole(id: number): Promise { return this.request('GET', `/roles/${id}`); } public async createRole(data: { display_name: string; description?: string; mfa_enforced?: boolean; external_auth_id?: string; permissions?: string[]; }): Promise { return this.request('POST', '/roles', data); } public async updateRole(id: number, data: { display_name?: string; description?: string; mfa_enforced?: boolean; external_auth_id?: string; permissions?: string[]; }): Promise { return this.request('PUT', `/roles/${id}`, data); } public async deleteRole(id: number): Promise { await this.request('DELETE', `/roles/${id}`); } // =========================================================================== // Search // =========================================================================== public async search(query: string, opts?: { page?: number; count?: number }): Promise { let url = `/search?query=${encodeURIComponent(query)}`; if (opts?.page) url += `&page=${opts.page}`; if (opts?.count) url += `&count=${opts.count}`; const result = await this.request>('GET', url); return result.data; } // =========================================================================== // Audit Log // =========================================================================== public async getAuditLog(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/audit-log', { ...opts, offset, count }), ), opts, ); } // =========================================================================== // Recycle Bin // =========================================================================== public async getRecycleBinItems(opts?: IBookStackListParams): Promise { return autoPaginate( (offset, count) => this.request>( 'GET', this.buildListUrl('/recycle-bin', { ...opts, offset, count }), ), opts, ); } public async restoreRecycleBinItem(deletionId: number): Promise<{ restore_count: number }> { return this.request<{ restore_count: number }>('PUT', `/recycle-bin/${deletionId}`); } public async destroyRecycleBinItem(deletionId: number): Promise<{ delete_count: number }> { return this.request<{ delete_count: number }>('DELETE', `/recycle-bin/${deletionId}`); } // =========================================================================== // Content Permissions // =========================================================================== public async getContentPermissions( contentType: 'page' | 'book' | 'chapter' | 'bookshelf', contentId: number, ): Promise { return this.request( 'GET', `/content-permissions/${contentType}/${contentId}`, ); } public async updateContentPermissions( contentType: 'page' | 'book' | 'chapter' | 'bookshelf', contentId: number, data: { owner_id?: number; role_permissions?: { role_id: number; view: boolean; create: boolean; update: boolean; delete: boolean; }[]; fallback_permissions?: { inheriting: boolean; view?: boolean; create?: boolean; update?: boolean; delete?: boolean; }; }, ): Promise { return this.request( 'PUT', `/content-permissions/${contentType}/${contentId}`, data, ); } // =========================================================================== // System // =========================================================================== public async getSystemInfo(): Promise { return this.request('GET', '/system'); } }