617 lines
20 KiB
TypeScript
617 lines
20 KiB
TypeScript
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<T = any>(
|
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
path: string,
|
|
data?: any,
|
|
): Promise<T> {
|
|
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')
|
|
.retry(3)
|
|
.handle429Backoff({ maxRetries: 5, maxWaitTime: 30000, fallbackDelay: 2000 });
|
|
|
|
if (data) {
|
|
builder = builder.json(data);
|
|
}
|
|
|
|
let response: Awaited<ReturnType<typeof builder.get>>;
|
|
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.status === 429) {
|
|
throw new Error(`Rate limited: ${method} ${path} — retries exhausted`);
|
|
}
|
|
|
|
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<string> {
|
|
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')
|
|
.retry(3)
|
|
.handle429Backoff({ maxRetries: 5, maxWaitTime: 30000, fallbackDelay: 2000 });
|
|
|
|
let response: Awaited<ReturnType<typeof builder.get>>;
|
|
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.status === 429) {
|
|
throw new Error(`Rate limited: ${method} ${path} — retries exhausted`);
|
|
}
|
|
|
|
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<Uint8Array> {
|
|
const url = `${this.baseUrl}/api${path}`;
|
|
const maxRetries = 3;
|
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
Authorization: `Token ${this.tokenId}:${this.tokenSecret}`,
|
|
},
|
|
});
|
|
if (response.status === 429) {
|
|
if (attempt === maxRetries) {
|
|
throw new Error(`Rate limited: GET ${path} — retries exhausted`);
|
|
}
|
|
const delay = 2000 * Math.pow(2, attempt);
|
|
await new Promise((r) => setTimeout(r, delay));
|
|
continue;
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(`GET ${path}: ${response.status} ${response.statusText}`);
|
|
}
|
|
const buf = await response.arrayBuffer();
|
|
return new Uint8Array(buf);
|
|
}
|
|
throw new Error(`GET ${path}: max retries exceeded`);
|
|
}
|
|
|
|
/** @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<ITestConnectionResult> {
|
|
try {
|
|
await this.request<IBookStackListResponse<IBookStackBook>>('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<BookStackBook[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackBook>>(
|
|
'GET',
|
|
this.buildListUrl('/books', { ...opts, offset, count }),
|
|
),
|
|
opts,
|
|
).then((books) => books.map((b) => new BookStackBook(this, b)));
|
|
}
|
|
|
|
public async getBook(id: number): Promise<BookStackBook> {
|
|
const raw = await this.request<IBookStackBook>('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<BookStackBook> {
|
|
const raw = await this.request<IBookStackBook>('POST', '/books', data);
|
|
return new BookStackBook(this, raw);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Chapters
|
|
// ===========================================================================
|
|
|
|
public async getChapters(opts?: IBookStackListParams): Promise<BookStackChapter[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackChapter>>(
|
|
'GET',
|
|
this.buildListUrl('/chapters', { ...opts, offset, count }),
|
|
),
|
|
opts,
|
|
).then((chapters) => chapters.map((c) => new BookStackChapter(this, c)));
|
|
}
|
|
|
|
public async getChapter(id: number): Promise<BookStackChapter> {
|
|
const raw = await this.request<IBookStackChapter>('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<BookStackChapter> {
|
|
const raw = await this.request<IBookStackChapter>('POST', '/chapters', data);
|
|
return new BookStackChapter(this, raw);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Pages
|
|
// ===========================================================================
|
|
|
|
public async getPages(opts?: IBookStackListParams): Promise<BookStackPage[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackPage>>(
|
|
'GET',
|
|
this.buildListUrl('/pages', { ...opts, offset, count }),
|
|
),
|
|
opts,
|
|
).then((pages) => pages.map((p) => new BookStackPage(this, p)));
|
|
}
|
|
|
|
public async getPage(id: number): Promise<BookStackPage> {
|
|
const raw = await this.request<IBookStackPage>('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<BookStackPage> {
|
|
const raw = await this.request<IBookStackPage>('POST', '/pages', data);
|
|
return new BookStackPage(this, raw);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Shelves
|
|
// ===========================================================================
|
|
|
|
public async getShelves(opts?: IBookStackListParams): Promise<BookStackShelf[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackShelf>>(
|
|
'GET',
|
|
this.buildListUrl('/shelves', { ...opts, offset, count }),
|
|
),
|
|
opts,
|
|
).then((shelves) => shelves.map((s) => new BookStackShelf(this, s)));
|
|
}
|
|
|
|
public async getShelf(id: number): Promise<BookStackShelf> {
|
|
const raw = await this.request<IBookStackShelf>('GET', `/shelves/${id}`);
|
|
return new BookStackShelf(this, raw);
|
|
}
|
|
|
|
public async createShelf(data: {
|
|
name: string;
|
|
description?: string;
|
|
description_html?: string;
|
|
books?: number[];
|
|
tags?: IBookStackTag[];
|
|
}): Promise<BookStackShelf> {
|
|
const raw = await this.request<IBookStackShelf>('POST', '/shelves', data);
|
|
return new BookStackShelf(this, raw);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Attachments
|
|
// ===========================================================================
|
|
|
|
public async getAttachments(opts?: IBookStackListParams): Promise<IBookStackAttachment[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackAttachment>>(
|
|
'GET',
|
|
this.buildListUrl('/attachments', { ...opts, offset, count }),
|
|
),
|
|
opts,
|
|
);
|
|
}
|
|
|
|
public async getAttachment(id: number): Promise<IBookStackAttachment> {
|
|
return this.request<IBookStackAttachment>('GET', `/attachments/${id}`);
|
|
}
|
|
|
|
public async createAttachment(data: {
|
|
name: string;
|
|
uploaded_to: number;
|
|
link?: string;
|
|
}): Promise<IBookStackAttachment> {
|
|
return this.request<IBookStackAttachment>('POST', '/attachments', data);
|
|
}
|
|
|
|
public async updateAttachment(id: number, data: {
|
|
name?: string;
|
|
uploaded_to?: number;
|
|
link?: string;
|
|
}): Promise<IBookStackAttachment> {
|
|
return this.request<IBookStackAttachment>('PUT', `/attachments/${id}`, data);
|
|
}
|
|
|
|
public async deleteAttachment(id: number): Promise<void> {
|
|
await this.request('DELETE', `/attachments/${id}`);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Comments
|
|
// ===========================================================================
|
|
|
|
public async getComments(opts?: IBookStackListParams): Promise<IBookStackComment[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackComment>>(
|
|
'GET',
|
|
this.buildListUrl('/comments', { ...opts, offset, count }),
|
|
),
|
|
opts,
|
|
);
|
|
}
|
|
|
|
public async getComment(id: number): Promise<IBookStackComment> {
|
|
return this.request<IBookStackComment>('GET', `/comments/${id}`);
|
|
}
|
|
|
|
public async createComment(data: {
|
|
page_id: number;
|
|
html: string;
|
|
reply_to?: number;
|
|
content_ref?: string;
|
|
}): Promise<IBookStackComment> {
|
|
return this.request<IBookStackComment>('POST', '/comments', data);
|
|
}
|
|
|
|
public async updateComment(id: number, data: {
|
|
html?: string;
|
|
archived?: boolean;
|
|
}): Promise<IBookStackComment> {
|
|
return this.request<IBookStackComment>('PUT', `/comments/${id}`, data);
|
|
}
|
|
|
|
public async deleteComment(id: number): Promise<void> {
|
|
await this.request('DELETE', `/comments/${id}`);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Image Gallery
|
|
// ===========================================================================
|
|
|
|
public async getImages(opts?: IBookStackListParams): Promise<IBookStackImage[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackImage>>(
|
|
'GET',
|
|
this.buildListUrl('/image-gallery', { ...opts, offset, count }),
|
|
),
|
|
opts,
|
|
);
|
|
}
|
|
|
|
public async getImage(id: number): Promise<IBookStackImage> {
|
|
return this.request<IBookStackImage>('GET', `/image-gallery/${id}`);
|
|
}
|
|
|
|
public async updateImage(id: number, data: {
|
|
name?: string;
|
|
}): Promise<IBookStackImage> {
|
|
return this.request<IBookStackImage>('PUT', `/image-gallery/${id}`, data);
|
|
}
|
|
|
|
public async deleteImage(id: number): Promise<void> {
|
|
await this.request('DELETE', `/image-gallery/${id}`);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Users
|
|
// ===========================================================================
|
|
|
|
public async getUsers(opts?: IBookStackListParams): Promise<IBookStackUser[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackUser>>(
|
|
'GET',
|
|
this.buildListUrl('/users', { ...opts, offset, count }),
|
|
),
|
|
opts,
|
|
);
|
|
}
|
|
|
|
public async getUser(id: number): Promise<IBookStackUser> {
|
|
return this.request<IBookStackUser>('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<IBookStackUser> {
|
|
return this.request<IBookStackUser>('POST', '/users', data);
|
|
}
|
|
|
|
public async updateUser(id: number, data: {
|
|
name?: string;
|
|
email?: string;
|
|
password?: string;
|
|
roles?: number[];
|
|
external_auth_id?: string;
|
|
language?: string;
|
|
}): Promise<IBookStackUser> {
|
|
return this.request<IBookStackUser>('PUT', `/users/${id}`, data);
|
|
}
|
|
|
|
public async deleteUser(id: number, opts?: { migrate_ownership_id?: number }): Promise<void> {
|
|
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<IBookStackRole[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackRole>>(
|
|
'GET',
|
|
this.buildListUrl('/roles', { ...opts, offset, count }),
|
|
),
|
|
opts,
|
|
);
|
|
}
|
|
|
|
public async getRole(id: number): Promise<IBookStackRole> {
|
|
return this.request<IBookStackRole>('GET', `/roles/${id}`);
|
|
}
|
|
|
|
public async createRole(data: {
|
|
display_name: string;
|
|
description?: string;
|
|
mfa_enforced?: boolean;
|
|
external_auth_id?: string;
|
|
permissions?: string[];
|
|
}): Promise<IBookStackRole> {
|
|
return this.request<IBookStackRole>('POST', '/roles', data);
|
|
}
|
|
|
|
public async updateRole(id: number, data: {
|
|
display_name?: string;
|
|
description?: string;
|
|
mfa_enforced?: boolean;
|
|
external_auth_id?: string;
|
|
permissions?: string[];
|
|
}): Promise<IBookStackRole> {
|
|
return this.request<IBookStackRole>('PUT', `/roles/${id}`, data);
|
|
}
|
|
|
|
public async deleteRole(id: number): Promise<void> {
|
|
await this.request('DELETE', `/roles/${id}`);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Search
|
|
// ===========================================================================
|
|
|
|
public async search(query: string, opts?: { page?: number; count?: number }): Promise<IBookStackSearchResult[]> {
|
|
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<IBookStackListResponse<IBookStackSearchResult>>('GET', url);
|
|
return result.data;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Audit Log
|
|
// ===========================================================================
|
|
|
|
public async getAuditLog(opts?: IBookStackListParams): Promise<IBookStackAuditLogEntry[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackAuditLogEntry>>(
|
|
'GET',
|
|
this.buildListUrl('/audit-log', { ...opts, offset, count }),
|
|
),
|
|
opts,
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Recycle Bin
|
|
// ===========================================================================
|
|
|
|
public async getRecycleBinItems(opts?: IBookStackListParams): Promise<IBookStackRecycleBinItem[]> {
|
|
return autoPaginate(
|
|
(offset, count) =>
|
|
this.request<IBookStackListResponse<IBookStackRecycleBinItem>>(
|
|
'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<IBookStackContentPermission> {
|
|
return this.request<IBookStackContentPermission>(
|
|
'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<IBookStackContentPermission> {
|
|
return this.request<IBookStackContentPermission>(
|
|
'PUT',
|
|
`/content-permissions/${contentType}/${contentId}`,
|
|
data,
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// System
|
|
// ===========================================================================
|
|
|
|
public async getSystemInfo(): Promise<IBookStackSystemInfo> {
|
|
return this.request<IBookStackSystemInfo>('GET', '/system');
|
|
}
|
|
}
|