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

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
.nogit/
# artifacts
coverage/
public/
# installs
node_modules/
# caches
.yarn/
.cache/
.rpt2_cache
# builds
dist/
dist_*/
#------# custom

30
.smartconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"@git.zone/cli": {
"projectType": "npm",
"module": {
"githost": "code.foss.global",
"gitscope": "apiclient.xyz",
"gitrepo": "bookstack",
"description": "A TypeScript API client for BookStack, providing easy access to books, chapters, pages, shelves, and more.",
"npmPackagename": "@apiclient.xyz/bookstack",
"license": "MIT",
"keywords": [
"bookstack",
"api client",
"TypeScript",
"wiki",
"documentation"
]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
},
"@ship.zone/szci": {
"npmGlobalTools": []
}
}

21
license Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Lossless GmbH (https://lossless.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

55
package.json Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "@apiclient.xyz/bookstack",
"version": "1.0.0",
"private": false,
"description": "A TypeScript API client for BookStack, providing easy access to books, chapters, pages, shelves, and more.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
"scripts": {
"test": "(tstest test/ --verbose --timeout 600)",
"build": "(tsbuild --web --allowimplicitany)"
},
"repository": {
"type": "git",
"url": "https://code.foss.global/apiclient.xyz/bookstack.git"
},
"keywords": [
"bookstack",
"api client",
"TypeScript",
"wiki",
"documentation",
"books",
"pages",
"chapters"
],
"author": "Lossless GmbH",
"license": "MIT",
"dependencies": {
"@push.rocks/smartrequest": "^5.0.1"
},
"devDependencies": {
"@git.zone/tsbuild": "^3.1.0",
"@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^2.8.2",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/tapbundle": "^5.6.0",
"@types/node": "^22.15.3"
},
"files": [
"ts/**/*",
"ts_web/**/*",
"dist/**/*",
"dist_*/**/*",
"dist_ts/**/*",
"dist_ts_web/**/*",
"assets/**/*",
"cli.js",
"npmextra.json",
"readme.md"
],
"browserslist": [
"last 1 chrome versions"
]
}

10808
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

4
qenv.yml Normal file
View File

@@ -0,0 +1,4 @@
required:
- BOOKSTACK_URL
- BOOKSTACK_TOKENID
- BOOKSTACK_TOKENSECRET

59
test/test.node.ts Normal file
View File

@@ -0,0 +1,59 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
import { BookStackAccount } from '../ts/index.js';
const testQenv = new qenv.Qenv('./', '.nogit/');
let bookstackAccount: BookStackAccount;
tap.test('should create a BookStackAccount instance', async () => {
const baseUrl = await testQenv.getEnvVarOnDemand('BOOKSTACK_URL');
const tokenId = await testQenv.getEnvVarOnDemand('BOOKSTACK_TOKENID');
const tokenSecret = await testQenv.getEnvVarOnDemand('BOOKSTACK_TOKENSECRET');
bookstackAccount = new BookStackAccount(baseUrl, tokenId, tokenSecret);
expect(bookstackAccount).toBeInstanceOf(BookStackAccount);
});
tap.test('should test connection', async () => {
const result = await bookstackAccount.testConnection();
expect(result).toHaveProperty('ok');
console.log('Connection test:', result);
});
tap.test('should get system info', async () => {
const info = await bookstackAccount.getSystemInfo();
expect(info).toHaveProperty('version');
console.log('System info:', info);
});
tap.test('should list books', async () => {
const books = await bookstackAccount.getBooks();
expect(books).toBeArray();
console.log(`Found ${books.length} books`);
});
tap.test('should list shelves', async () => {
const shelves = await bookstackAccount.getShelves();
expect(shelves).toBeArray();
console.log(`Found ${shelves.length} shelves`);
});
tap.test('should list chapters', async () => {
const chapters = await bookstackAccount.getChapters();
expect(chapters).toBeArray();
console.log(`Found ${chapters.length} chapters`);
});
tap.test('should list pages', async () => {
const pages = await bookstackAccount.getPages();
expect(pages).toBeArray();
console.log(`Found ${pages.length} pages`);
});
tap.test('should search content', async () => {
const results = await bookstackAccount.search('test');
expect(results).toBeArray();
console.log(`Found ${results.length} search results`);
});
export default tap.start();

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';

16
tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
},
"exclude": [
"dist_*/**/*.d.ts"
]
}