This commit is contained in:
2026-03-28 09:16:54 +00:00
commit 692e45286e
18 changed files with 12509 additions and 0 deletions

5
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,5 @@
export const commitinfo = {
name: '@apiclient.xyz/bookstack',
version: '1.0.0',
description: 'A TypeScript API client for BookStack, providing easy access to books, chapters, pages, shelves, and more.',
};

View File

@@ -0,0 +1,592 @@
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');
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.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');
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.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 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<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');
}
}

View File

@@ -0,0 +1,155 @@
import type { BookStackAccount } from './bookstack.classes.account.js';
import type {
IBookStackBook,
IBookStackChapter,
IBookStackPage,
IBookStackTag,
IBookStackListParams,
IBookStackListResponse,
TBookStackExportFormat,
} from './bookstack.interfaces.js';
import { BookStackChapter } from './bookstack.classes.chapter.js';
import { BookStackPage } from './bookstack.classes.page.js';
import { autoPaginate } from './bookstack.helpers.js';
export class BookStackBook {
public readonly id: number;
public readonly name: string;
public readonly slug: string;
public readonly description: string;
public readonly createdAt: string;
public readonly updatedAt: string;
public readonly createdBy: number;
public readonly updatedBy: number;
public readonly ownedBy: number;
public readonly defaultTemplateId: number | null;
public readonly tags: IBookStackTag[];
/** @internal */
constructor(
private accountRef: BookStackAccount,
raw: IBookStackBook,
) {
this.id = raw.id;
this.name = raw.name || '';
this.slug = raw.slug || '';
this.description = raw.description || '';
this.createdAt = raw.created_at || '';
this.updatedAt = raw.updated_at || '';
this.createdBy = raw.created_by;
this.updatedBy = raw.updated_by;
this.ownedBy = raw.owned_by;
this.defaultTemplateId = raw.default_template_id ?? null;
this.tags = raw.tags || [];
}
// ---------------------------------------------------------------------------
// CRUD
// ---------------------------------------------------------------------------
async update(data: {
name?: string;
description?: string;
description_html?: string;
tags?: IBookStackTag[];
default_template_id?: number | null;
}): Promise<BookStackBook> {
const raw = await this.accountRef.request<IBookStackBook>('PUT', `/books/${this.id}`, data);
return new BookStackBook(this.accountRef, raw);
}
async delete(): Promise<void> {
await this.accountRef.request('DELETE', `/books/${this.id}`);
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
async export(format: TBookStackExportFormat): Promise<string | Uint8Array> {
if (format === 'pdf') {
return this.accountRef.requestBinary(`/books/${this.id}/export/${format}`);
}
return this.accountRef.requestText('GET', `/books/${this.id}/export/${format}`);
}
// ---------------------------------------------------------------------------
// Navigation — Chapters
// ---------------------------------------------------------------------------
async getChapters(opts?: IBookStackListParams): Promise<BookStackChapter[]> {
return autoPaginate(
(offset, count) =>
this.accountRef.request<IBookStackListResponse<IBookStackChapter>>(
'GET',
this.accountRef.buildListUrl(`/chapters`, { ...opts, offset, count, filter: { ...opts?.filter, book_id: String(this.id) } }),
),
opts,
).then((chapters) => chapters.map((c) => new BookStackChapter(this.accountRef, c)));
}
async createChapter(data: {
name: string;
description?: string;
description_html?: string;
tags?: IBookStackTag[];
priority?: number;
default_template_id?: number | null;
}): Promise<BookStackChapter> {
const raw = await this.accountRef.request<IBookStackChapter>('POST', '/chapters', {
book_id: this.id,
...data,
});
return new BookStackChapter(this.accountRef, raw);
}
// ---------------------------------------------------------------------------
// Navigation — Pages
// ---------------------------------------------------------------------------
async getPages(opts?: IBookStackListParams): Promise<BookStackPage[]> {
return autoPaginate(
(offset, count) =>
this.accountRef.request<IBookStackListResponse<IBookStackPage>>(
'GET',
this.accountRef.buildListUrl(`/pages`, { ...opts, offset, count, filter: { ...opts?.filter, book_id: String(this.id) } }),
),
opts,
).then((pages) => pages.map((p) => new BookStackPage(this.accountRef, p)));
}
async createPage(data: {
name: string;
html?: string;
markdown?: string;
chapter_id?: number;
tags?: IBookStackTag[];
priority?: number;
}): Promise<BookStackPage> {
const raw = await this.accountRef.request<IBookStackPage>('POST', '/pages', {
book_id: this.id,
...data,
});
return new BookStackPage(this.accountRef, raw);
}
// ---------------------------------------------------------------------------
// Serialization
// ---------------------------------------------------------------------------
toJSON(): IBookStackBook {
return {
id: this.id,
name: this.name,
slug: this.slug,
description: this.description,
created_at: this.createdAt,
updated_at: this.updatedAt,
created_by: this.createdBy,
updated_by: this.updatedBy,
owned_by: this.ownedBy,
default_template_id: this.defaultTemplateId,
tags: this.tags,
};
}
}

View File

@@ -0,0 +1,127 @@
import type { BookStackAccount } from './bookstack.classes.account.js';
import type {
IBookStackChapter,
IBookStackPage,
IBookStackTag,
IBookStackListParams,
IBookStackListResponse,
TBookStackExportFormat,
} from './bookstack.interfaces.js';
import { BookStackPage } from './bookstack.classes.page.js';
import { autoPaginate } from './bookstack.helpers.js';
export class BookStackChapter {
public readonly id: number;
public readonly bookId: number;
public readonly name: string;
public readonly slug: string;
public readonly description: string;
public readonly priority: number;
public readonly createdAt: string;
public readonly updatedAt: string;
public readonly createdBy: number;
public readonly updatedBy: number;
public readonly ownedBy: number;
public readonly tags: IBookStackTag[];
/** @internal */
constructor(
private accountRef: BookStackAccount,
raw: IBookStackChapter,
) {
this.id = raw.id;
this.bookId = raw.book_id;
this.name = raw.name || '';
this.slug = raw.slug || '';
this.description = raw.description || '';
this.priority = raw.priority || 0;
this.createdAt = raw.created_at || '';
this.updatedAt = raw.updated_at || '';
this.createdBy = raw.created_by;
this.updatedBy = raw.updated_by;
this.ownedBy = raw.owned_by;
this.tags = raw.tags || [];
}
// ---------------------------------------------------------------------------
// CRUD
// ---------------------------------------------------------------------------
async update(data: {
name?: string;
description?: string;
description_html?: string;
book_id?: number;
tags?: IBookStackTag[];
priority?: number;
default_template_id?: number | null;
}): Promise<BookStackChapter> {
const raw = await this.accountRef.request<IBookStackChapter>('PUT', `/chapters/${this.id}`, data);
return new BookStackChapter(this.accountRef, raw);
}
async delete(): Promise<void> {
await this.accountRef.request('DELETE', `/chapters/${this.id}`);
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
async export(format: TBookStackExportFormat): Promise<string | Uint8Array> {
if (format === 'pdf') {
return this.accountRef.requestBinary(`/chapters/${this.id}/export/${format}`);
}
return this.accountRef.requestText('GET', `/chapters/${this.id}/export/${format}`);
}
// ---------------------------------------------------------------------------
// Navigation — Pages
// ---------------------------------------------------------------------------
async getPages(opts?: IBookStackListParams): Promise<BookStackPage[]> {
return autoPaginate(
(offset, count) =>
this.accountRef.request<IBookStackListResponse<IBookStackPage>>(
'GET',
this.accountRef.buildListUrl(`/pages`, { ...opts, offset, count, filter: { ...opts?.filter, chapter_id: String(this.id) } }),
),
opts,
).then((pages) => pages.map((p) => new BookStackPage(this.accountRef, p)));
}
async createPage(data: {
name: string;
html?: string;
markdown?: string;
tags?: IBookStackTag[];
priority?: number;
}): Promise<BookStackPage> {
const raw = await this.accountRef.request<IBookStackPage>('POST', '/pages', {
chapter_id: this.id,
...data,
});
return new BookStackPage(this.accountRef, raw);
}
// ---------------------------------------------------------------------------
// Serialization
// ---------------------------------------------------------------------------
toJSON(): IBookStackChapter {
return {
id: this.id,
book_id: this.bookId,
name: this.name,
slug: this.slug,
description: this.description,
priority: this.priority,
created_at: this.createdAt,
updated_at: this.updatedAt,
created_by: this.createdBy,
updated_by: this.updatedBy,
owned_by: this.ownedBy,
tags: this.tags,
};
}
}

View File

@@ -0,0 +1,135 @@
import type { BookStackAccount } from './bookstack.classes.account.js';
import type {
IBookStackPage,
IBookStackTag,
IBookStackComment,
TBookStackExportFormat,
} from './bookstack.interfaces.js';
export class BookStackPage {
public readonly id: number;
public readonly bookId: number;
public readonly chapterId: number;
public readonly name: string;
public readonly slug: string;
public readonly html: string;
public readonly markdown: string;
public readonly priority: number;
public readonly draft: boolean;
public readonly template: boolean;
public readonly revisionCount: number;
public readonly editor: string;
public readonly createdAt: string;
public readonly updatedAt: string;
public readonly createdBy: number;
public readonly updatedBy: number;
public readonly ownedBy: number;
public readonly tags: IBookStackTag[];
/** @internal */
constructor(
private accountRef: BookStackAccount,
raw: IBookStackPage,
) {
this.id = raw.id;
this.bookId = raw.book_id;
this.chapterId = raw.chapter_id;
this.name = raw.name || '';
this.slug = raw.slug || '';
this.html = raw.html || '';
this.markdown = raw.markdown || '';
this.priority = raw.priority || 0;
this.draft = raw.draft || false;
this.template = raw.template || false;
this.revisionCount = raw.revision_count || 0;
this.editor = raw.editor || '';
this.createdAt = raw.created_at || '';
this.updatedAt = raw.updated_at || '';
this.createdBy = raw.created_by;
this.updatedBy = raw.updated_by;
this.ownedBy = raw.owned_by;
this.tags = raw.tags || [];
}
// ---------------------------------------------------------------------------
// CRUD
// ---------------------------------------------------------------------------
async update(data: {
name?: string;
html?: string;
markdown?: string;
book_id?: number;
chapter_id?: number;
tags?: IBookStackTag[];
priority?: number;
}): Promise<BookStackPage> {
const raw = await this.accountRef.request<IBookStackPage>('PUT', `/pages/${this.id}`, data);
return new BookStackPage(this.accountRef, raw);
}
async delete(): Promise<void> {
await this.accountRef.request('DELETE', `/pages/${this.id}`);
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
async export(format: TBookStackExportFormat): Promise<string | Uint8Array> {
if (format === 'pdf') {
return this.accountRef.requestBinary(`/pages/${this.id}/export/${format}`);
}
return this.accountRef.requestText('GET', `/pages/${this.id}/export/${format}`);
}
// ---------------------------------------------------------------------------
// Comments
// ---------------------------------------------------------------------------
async getComments(): Promise<IBookStackComment[]> {
const result = await this.accountRef.request<{ data: IBookStackComment[] }>(
'GET',
`/comments?filter[page_id]=${this.id}`,
);
return result.data;
}
async addComment(data: {
html: string;
reply_to?: number;
content_ref?: string;
}): Promise<IBookStackComment> {
return this.accountRef.request<IBookStackComment>('POST', '/comments', {
page_id: this.id,
...data,
});
}
// ---------------------------------------------------------------------------
// Serialization
// ---------------------------------------------------------------------------
toJSON(): IBookStackPage {
return {
id: this.id,
book_id: this.bookId,
chapter_id: this.chapterId,
name: this.name,
slug: this.slug,
html: this.html,
markdown: this.markdown,
priority: this.priority,
draft: this.draft,
template: this.template,
revision_count: this.revisionCount,
editor: this.editor,
created_at: this.createdAt,
updated_at: this.updatedAt,
created_by: this.createdBy,
updated_by: this.updatedBy,
owned_by: this.ownedBy,
tags: this.tags,
};
}
}

View File

@@ -0,0 +1,91 @@
import type { BookStackAccount } from './bookstack.classes.account.js';
import type {
IBookStackShelf,
IBookStackBook,
IBookStackTag,
IBookStackListParams,
IBookStackListResponse,
} from './bookstack.interfaces.js';
import { BookStackBook } from './bookstack.classes.book.js';
import { autoPaginate } from './bookstack.helpers.js';
export class BookStackShelf {
public readonly id: number;
public readonly name: string;
public readonly slug: string;
public readonly description: string;
public readonly createdAt: string;
public readonly updatedAt: string;
public readonly createdBy: number;
public readonly updatedBy: number;
public readonly ownedBy: number;
public readonly tags: IBookStackTag[];
/** @internal */
constructor(
private accountRef: BookStackAccount,
raw: IBookStackShelf,
) {
this.id = raw.id;
this.name = raw.name || '';
this.slug = raw.slug || '';
this.description = raw.description || '';
this.createdAt = raw.created_at || '';
this.updatedAt = raw.updated_at || '';
this.createdBy = raw.created_by;
this.updatedBy = raw.updated_by;
this.ownedBy = raw.owned_by;
this.tags = raw.tags || [];
}
// ---------------------------------------------------------------------------
// CRUD
// ---------------------------------------------------------------------------
async update(data: {
name?: string;
description?: string;
description_html?: string;
books?: number[];
tags?: IBookStackTag[];
}): Promise<BookStackShelf> {
const raw = await this.accountRef.request<IBookStackShelf>('PUT', `/shelves/${this.id}`, data);
return new BookStackShelf(this.accountRef, raw);
}
async delete(): Promise<void> {
await this.accountRef.request('DELETE', `/shelves/${this.id}`);
}
// ---------------------------------------------------------------------------
// Navigation — Books
// ---------------------------------------------------------------------------
async getBooks(opts?: IBookStackListParams): Promise<BookStackBook[]> {
// The shelf detail endpoint includes books inline
const detail = await this.accountRef.request<IBookStackShelf>('GET', `/shelves/${this.id}`);
if (detail.books) {
return detail.books.map((b) => new BookStackBook(this.accountRef, b));
}
return [];
}
// ---------------------------------------------------------------------------
// Serialization
// ---------------------------------------------------------------------------
toJSON(): IBookStackShelf {
return {
id: this.id,
name: this.name,
slug: this.slug,
description: this.description,
created_at: this.createdAt,
updated_at: this.updatedAt,
created_by: this.createdBy,
updated_by: this.updatedBy,
owned_by: this.ownedBy,
tags: this.tags,
};
}
}

29
ts/bookstack.helpers.ts Normal file
View File

@@ -0,0 +1,29 @@
import type { IBookStackListResponse } from './bookstack.interfaces.js';
/**
* Auto-paginate a BookStack list endpoint using offset/count.
* If opts includes a specific offset, returns just that single page (no auto-pagination).
*/
export async function autoPaginate<T>(
fetchPage: (offset: number, count: number) => Promise<IBookStackListResponse<T>>,
opts?: { offset?: number; count?: number },
): Promise<T[]> {
const count = opts?.count || 100;
// If caller requests a specific offset, return just that page
if (opts?.offset !== undefined) {
const result = await fetchPage(opts.offset, count);
return result.data;
}
// Otherwise auto-paginate through all pages
const all: T[] = [];
let offset = 0;
while (true) {
const result = await fetchPage(offset, count);
all.push(...result.data);
offset += count;
if (offset >= result.total) break;
}
return all;
}

322
ts/bookstack.interfaces.ts Normal file
View File

@@ -0,0 +1,322 @@
// ---------------------------------------------------------------------------
// Common
// ---------------------------------------------------------------------------
export interface ITestConnectionResult {
ok: boolean;
error?: string;
}
export interface IBookStackListParams {
count?: number;
offset?: number;
sort?: string;
filter?: Record<string, string>;
}
export interface IBookStackListResponse<T> {
data: T[];
total: number;
}
export interface IBookStackErrorResponse {
error: {
code: number;
message: string;
};
}
export interface IBookStackTag {
name: string;
value: string;
order?: number;
}
export type TBookStackExportFormat = 'html' | 'pdf' | 'plaintext' | 'markdown';
// ---------------------------------------------------------------------------
// Books
// ---------------------------------------------------------------------------
export interface IBookStackBook {
id: number;
name: string;
slug: string;
description: string;
description_html?: string;
created_at: string;
updated_at: string;
created_by: number;
updated_by: number;
owned_by: number;
default_template_id: number | null;
tags?: IBookStackTag[];
cover?: { id: number; name: string; url: string } | null;
contents?: IBookStackBookContent[];
}
export interface IBookStackBookContent {
id: number;
name: string;
slug: string;
type: 'chapter' | 'page';
book_id: number;
priority: number;
created_at: string;
updated_at: string;
url: string;
pages?: IBookStackBookContent[];
}
// ---------------------------------------------------------------------------
// Chapters
// ---------------------------------------------------------------------------
export interface IBookStackChapter {
id: number;
book_id: number;
name: string;
slug: string;
description: string;
description_html?: string;
priority: number;
created_at: string;
updated_at: string;
created_by: number;
updated_by: number;
owned_by: number;
default_template_id?: number | null;
tags?: IBookStackTag[];
pages?: IBookStackPage[];
}
// ---------------------------------------------------------------------------
// Pages
// ---------------------------------------------------------------------------
export interface IBookStackPage {
id: number;
book_id: number;
chapter_id: number;
name: string;
slug: string;
html: string;
raw_html?: string;
markdown: string;
priority: number;
draft: boolean;
template: boolean;
created_at: string;
updated_at: string;
created_by: number;
updated_by: number;
owned_by: number;
revision_count: number;
editor: string;
book_slug?: string;
tags?: IBookStackTag[];
}
// ---------------------------------------------------------------------------
// Shelves
// ---------------------------------------------------------------------------
export interface IBookStackShelf {
id: number;
name: string;
slug: string;
description: string;
description_html?: string;
created_at: string;
updated_at: string;
created_by: number;
updated_by: number;
owned_by: number;
tags?: IBookStackTag[];
cover?: { id: number; name: string; url: string } | null;
books?: IBookStackBook[];
}
// ---------------------------------------------------------------------------
// Attachments
// ---------------------------------------------------------------------------
export interface IBookStackAttachment {
id: number;
name: string;
extension: string;
uploaded_to: number;
external: boolean;
order: number;
created_at: string;
updated_at: string;
created_by: number;
updated_by: number;
content?: string;
links?: {
html: string;
markdown: string;
};
}
// ---------------------------------------------------------------------------
// Comments
// ---------------------------------------------------------------------------
export interface IBookStackComment {
id: number;
html: string;
parent_id: number | null;
local_id: number;
content_ref: string;
created_at: string;
updated_at: string;
created_by: number;
updated_by: number;
archived?: boolean;
replies?: IBookStackComment[];
}
// ---------------------------------------------------------------------------
// Image Gallery
// ---------------------------------------------------------------------------
export interface IBookStackImage {
id: number;
name: string;
url: string;
path: string;
type: string;
uploaded_to: number;
created_at: string;
updated_at: string;
created_by: number;
updated_by: number;
thumbs?: {
gallery: string;
display: string;
};
content?: {
html: string;
markdown: string;
};
}
// ---------------------------------------------------------------------------
// Users
// ---------------------------------------------------------------------------
export interface IBookStackUser {
id: number;
name: string;
slug: string;
email: string;
profile_url: string;
edit_url?: string;
avatar_url: string;
external_auth_id: string;
created_at: string;
updated_at: string;
last_activity_at?: string;
roles?: { id: number; display_name: string }[];
}
// ---------------------------------------------------------------------------
// Roles
// ---------------------------------------------------------------------------
export interface IBookStackRole {
id: number;
display_name: string;
description: string;
mfa_enforced: boolean;
external_auth_id: string;
permissions_count: number;
users_count?: number;
created_at: string;
updated_at: string;
permissions?: string[];
users?: IBookStackUser[];
}
// ---------------------------------------------------------------------------
// Search
// ---------------------------------------------------------------------------
export interface IBookStackSearchResult {
id: number;
name: string;
slug: string;
book_id?: number;
chapter_id?: number;
type: string;
url: string;
preview_html?: { name: string; content: string };
tags?: IBookStackTag[];
created_at: string;
updated_at: string;
}
// ---------------------------------------------------------------------------
// Audit Log
// ---------------------------------------------------------------------------
export interface IBookStackAuditLogEntry {
id: number;
type: string;
detail: string;
user_id: number;
loggable_id: number;
loggable_type: string;
ip: string;
created_at: string;
user?: IBookStackUser;
}
// ---------------------------------------------------------------------------
// Recycle Bin
// ---------------------------------------------------------------------------
export interface IBookStackRecycleBinItem {
id: number;
deleted_by: number;
created_at: string;
updated_at: string;
deletable_type: string;
deletable_id: number;
deletable?: Record<string, any>;
}
// ---------------------------------------------------------------------------
// Content Permissions
// ---------------------------------------------------------------------------
export interface IBookStackContentPermission {
owner: IBookStackUser;
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;
};
}
// ---------------------------------------------------------------------------
// System
// ---------------------------------------------------------------------------
export interface IBookStackSystemInfo {
version: string;
instance_id: string;
app_name: string;
app_logo: string | null;
base_url: string;
}

3
ts/bookstack.plugins.ts Normal file
View File

@@ -0,0 +1,3 @@
import * as smartrequest from '@push.rocks/smartrequest';
export { smartrequest };

38
ts/index.ts Normal file
View File

@@ -0,0 +1,38 @@
// Main client
export { BookStackAccount } from './bookstack.classes.account.js';
// Domain classes
export { BookStackBook } from './bookstack.classes.book.js';
export { BookStackChapter } from './bookstack.classes.chapter.js';
export { BookStackPage } from './bookstack.classes.page.js';
export { BookStackShelf } from './bookstack.classes.shelf.js';
// Helpers
export { autoPaginate } from './bookstack.helpers.js';
// Interfaces (raw API types)
export type {
IBookStackBook,
IBookStackChapter,
IBookStackPage,
IBookStackShelf,
IBookStackAttachment,
IBookStackComment,
IBookStackImage,
IBookStackUser,
IBookStackRole,
IBookStackSearchResult,
IBookStackAuditLogEntry,
IBookStackRecycleBinItem,
IBookStackContentPermission,
IBookStackSystemInfo,
IBookStackListResponse,
IBookStackListParams,
IBookStackErrorResponse,
IBookStackTag,
TBookStackExportFormat,
ITestConnectionResult,
} from './bookstack.interfaces.js';
// Commit info
export { commitinfo } from './00_commitinfo_data.js';