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
View File
@@ -0,0 +1,19 @@
.nogit/
# artifacts
coverage/
public/
# installs
node_modules/
# caches
.yarn/
.cache/
.rpt2_cache
# builds
dist/
dist_*/
#------# custom
+30
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
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
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
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
required:
- BOOKSTACK_URL
- BOOKSTACK_TOKENID
- BOOKSTACK_TOKENSECRET
+59
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
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.',
};
+592
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');
}
}
+155
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,
};
}
}
+127
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,
};
}
}
+135
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,
};
}
}
+91
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
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
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
View File
@@ -0,0 +1,3 @@
import * as smartrequest from '@push.rocks/smartrequest';
export { smartrequest };
+38
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
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"
]
}