diff --git a/CHANGELOG.md b/CHANGELOG.md index 4862caf..b37a631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.4.0](https://github.com/lucaguindani/n8n-nodes-bookstack/compare/1.3.0...1.4.0) + +> 8 April 2026 + +##### Features +- Make `name` field optional on Create operations for Page, Book, Chapter, and Shelf resources +- Add `generateFallbackName()` for auto-generation of names from content (extracts headings from HTML/markdown for pages, uses description for other resources, falls back to timestamp) +- Add `decodeHtmlEntities()` for clean auto-generated names from HTML content +- Improve all operation action texts for better AI agent guidance via MCP +- Enrich field descriptions with move semantics, search syntax hints, and tag usage guidance +- Update Global Search description with BookStack advanced filter syntax (`{type:page}`, `{tag:name}`, `{in_name:text}`, `{in_body:text}`) +- Encode token-efficient navigation strategy in all tool descriptions (Search first, Get by ID, never Get Many with Return All on large instances) +- Add warnings to Return All, Deep Dive, and Get Many operations about token cost +- Recommend Markdown over HTML in page content descriptions (~3x fewer tokens) + +##### QA & Documentation +- Add `CLAUDE.md` project knowledge base for AI-assisted development +- Add `KNOWN_ISSUES.md` documenting 16 pre-existing issues found during QA audit +- Guard against whitespace-only markdown headings in fallback name generation +- Limit HTML text extraction to 2000-char prefix for performance + #### [1.3.0](https://github.com/lucaguindani/n8n-nodes-bookstack/compare/1.2.0...1.3.0) - Bump flatted in the npm_and_yarn group across 1 directory [`#6`](https://github.com/lucaguindani/n8n-nodes-bookstack/pull/6) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..45cf52c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,430 @@ +# CLAUDE.md - Project Knowledge Base for AI-Assisted Development + +This file contains essential context for LLMs working on this project. +Read this first before making any changes. + +--- + +## Project Overview + +**n8n-nodes-bookstack** is a community n8n node that integrates with the BookStack API. +It provides CRUD operations for BookStack's content hierarchy and is designed to be +used both manually in n8n workflows and by AI agents via MCP (`usableAsTool: true`). + +- **Version**: 1.4.0 +- **License**: MIT +- **Runtime**: Node 20+, n8n 1.109+, BookStack 24.5+ +- **Package Manager**: pnpm + +--- + +## BookStack Content Hierarchy + +``` +Shelf (top-level container) + └── Book (contains chapters and/or direct pages) + ├── Chapter (groups related pages within a book) + │ └── Page (content unit with HTML or Markdown body) + └── Page (direct page without chapter) +``` + +- A **Shelf** holds multiple **Books** (many-to-many via book IDs) +- A **Book** contains **Chapters** and/or direct **Pages** +- A **Chapter** belongs to exactly one Book and contains **Pages** +- A **Page** belongs to either a Book or a Chapter (not both simultaneously) +- **Tags** can be applied to Books, Chapters, Pages, and Shelves + +--- + +## Architecture & File Structure + +``` +credentials/ + BookstackApi.credentials.ts # API auth (Token ID + Token Secret) + +nodes/Bookstack/ + Bookstack.node.ts # Main node class (all operation handlers) + Bookstack.node.json # Node metadata for n8n registry + descriptions/ + ResourceProperty.ts # Resource selector (dropdown) + Book.description.ts # Book CRUD field definitions + Chapter.description.ts # Chapter CRUD field definitions + Page.description.ts # Page CRUD field definitions + Shelf.description.ts # Shelf CRUD field definitions + Attachment.description.ts # Attachment CRUD (file/link) + Image.description.ts # Image gallery CRUD + Global.description.ts # Global Search + Audit Log + ListOperations.ts # Shared filtering/sorting/pagination + types/ + BookstackTypes.ts # Filter interface + utils/ + BookstackApiHelpers.ts # HTTP request functions + validation +``` + +--- + +## Key Design Patterns + +### 1. Node Class Structure (`Bookstack.node.ts`) + +The `Bookstack` class implements `INodeType` with: + +- **`resourceEndpoints`**: Maps resource names to API paths + - `book` -> `books`, `page` -> `pages`, `chapter` -> `chapters`, + `shelf` -> `shelves`, `attachment` -> `attachments`, `image` -> `image-gallery` + +- **`resourceFields`**: Lists writable fields per resource + - `book`: name, description, tags, default_template_id + - `page`: name, html, markdown, book_id, chapter_id, tags + - `chapter`: name, description, book_id, tags + - `shelf`: name, description, books, tags + +- **`buildRequestBody()`**: Reads all fields for a resource, skips undefined/empty values. + Converts tags from comma-separated string to `[{name: "tag"}]` array format. + Converts shelf books from comma-separated string to integer array. + +- **`generateFallbackName()`**: Auto-generates a name when not provided: + - Pages: First HTML heading (h1-h6) -> first text content -> first markdown heading -> first line -> timestamp + - Books/Chapters/Shelves: Description text (truncated to 255) -> timestamp + - Fallback format: `{resource}-{ISO-timestamp}` + +- **`validatePageCreation()`**: Ensures pages have either book_id or chapter_id, and either html or markdown. + +### 2. Operation Flow + +``` +execute() -> for each item: + 1. Read resource + operation from parameters + 2. Route to handler: + - book/page/shelf/chapter: handleCreateOperation / handleGetOperation / etc. + - attachment: handleCreateAttachmentOperation (multipart) + - image: handleCreateImageOperation (multipart) + - global: handleSearchOperation / handleAuditLogOperation + 3. Handler builds request body, validates, calls API + 4. Response mapped to INodeExecutionData +``` + +### 3. Description Files Pattern + +Each resource has two exports: +- `{resource}Operations`: Operation dropdown (Create/Delete/Get/GetMany/Update) +- `{resource}Fields`: Parameter definitions with `displayOptions` controlling visibility + +**Important for AI/MCP**: The `action` string on each operation option becomes the tool +action description. The `description` string on each field is what AI agents read to +understand parameters. Both should be clear and actionable. + +### 4. Optional vs Required Fields + +- Fields with `required: true` become mandatory MCP tool parameters (AI must provide them) +- Fields without `required` are optional - AI can skip them +- The `default: ''` value is for UI initialization only; `getNodeParameter()` with + fallback `undefined` returns `undefined` when the user/AI doesn't provide a value +- `buildRequestBody()` skips fields where `value === undefined || value === ''` + +### 5. Auto-Name Generation (v1.4.0) + +The `name` field is optional on Create operations. The BookStack API requires it, +so `handleCreateOperation()` calls `generateFallbackName()` when `body.name` is falsy. +This allows AI agents to either: +- Generate their own name (guided by the field description) +- Omit the name entirely (fallback auto-generation kicks in) + +--- + +## BookStack API Reference + +### Authentication +``` +Authorization: Token {tokenId}:{tokenSecret} +``` +Base URL includes `/api` path (e.g., `https://bookstack.example.com/api`). + +### Core Endpoints + +| Method | Endpoint | Notes | +|--------|----------|-------| +| GET | /books | List books (supports count, offset, sort, filter) | +| GET | /books/{id} | Returns book with `contents` array (chapters + direct pages) | +| POST | /books | Create book. Required: `name` | +| PUT | /books/{id} | Update book | +| DELETE | /books/{id} | Delete book | +| GET | /chapters | List chapters | +| GET | /chapters/{id} | Returns chapter with `pages` array | +| POST | /chapters | Create chapter. Required: `name`, `book_id` | +| PUT | /chapters/{id} | Update chapter. Change `book_id` to MOVE chapter | +| DELETE | /chapters/{id} | Delete chapter | +| GET | /pages | List pages | +| GET | /pages/{id} | Returns page with full html/markdown content | +| POST | /pages | Create page. Required: `name`, `book_id` OR `chapter_id`, `html` OR `markdown` | +| PUT | /pages/{id} | Update page. Change `book_id`/`chapter_id` to MOVE page | +| DELETE | /pages/{id} | Delete page | +| GET | /shelves | List shelves | +| GET | /shelves/{id} | Returns shelf with `books` array | +| POST | /shelves | Create shelf. Required: `name` | +| PUT | /shelves/{id} | Update shelf | +| DELETE | /shelves/{id} | Delete shelf | +| GET | /search | Full-text search. Query param: `query` | +| GET | /audit-log | Activity log (requires admin permissions) | +| GET/POST/PUT/DELETE | /attachments/* | File/link attachments on pages | +| GET/POST/PUT/DELETE | /image-gallery/* | Image uploads for pages | + +### Search Query Syntax + +BookStack search supports inline filters appended to the query string: +- `{type:page}` / `{type:book}` / `{type:chapter}` / `{type:bookshelf}` - filter by content type +- `{in_name:text}` - search only in names/titles +- `{in_body:text}` - search only in body content +- `{tag:tagname}` - filter by tag name +- `{tag:name=value}` - filter by tag name-value pair +- `{created_by:user_id}` - filter by creator +- `{updated_by:user_id}` - filter by last editor +- `{is_restricted}` - only restricted content +- `{viewed_by_me}` / `{not_viewed_by_me}` - personal view history + +### Pagination + +All list endpoints support: +- `count`: Items per page (max 500) +- `offset`: Skip N items +- `sort`: Field name prefixed with `+` (asc) or `-` (desc) +- `filter[field:op]`: Field-level filtering (eq, ne, gt, gte, lt, lte, like) + +### Markdown vs HTML (Important for AI Usage) + +**Always prefer Markdown over HTML for page content.** Reasons: +- Markdown uses ~3x fewer tokens than equivalent HTML +- AI agents process Markdown more efficiently (less noise from tags) +- BookStack stores both formats but only one should be set per request +- **Do NOT set both `html` and `markdown`** in the same create/update call - BookStack + will use one and ignore the other, behavior is undefined + +The `html` field exists as a fallback for users who need precise formatting control +or cannot use Markdown. For AI-driven workflows, Markdown is the gold standard. + +### Field Constraints + +- `name`: max 255 characters (all resources) +- `description`: max 1900 characters (books, chapters, shelves) +- `tags`: Array of `{name: string, value?: string}` objects +- `books` (shelf): Array of integer book IDs + +### API Endpoints NOT Implemented in This Node + +These BookStack API endpoints exist but are not yet in the node: +- `GET /pages/{id}/export/html` - Export page as HTML +- `GET /pages/{id}/export/markdown` - Export page as Markdown +- `GET /pages/{id}/export/plaintext` - Export page as plain text +- `GET /books/{id}/export/html` - Export book as HTML +- `GET /books/{id}/export/pdf` - Export book as PDF +- `GET /chapters/{id}/export/html` - Export chapter as HTML +- Content comments API (if available in BookStack version) +- User/Role management endpoints +- Recycle bin endpoints +- Webhook management endpoints + +--- + +## AI Agent Workflow (Intended Use Case) + +The primary AI use case is automated content organization: + +1. **Webhook triggers** when new content is created in BookStack (e.g., a note dropped into an "inbox" shelf) +2. **AI agent reads** the new content via Get Page +3. **AI searches** for where content belongs using Global Search with type filters +4. **AI navigates** the hierarchy efficiently: Get Shelf -> Get Book -> Get Chapter +5. **AI decides** to: + - Move the page (Update Page with new `chapter_id` or `book_id`) + - Merge with existing content (Update existing page, delete duplicate) + - Create new structure (Create Chapter/Book if needed) + - Rename the page (Update with new `name`) + - Tag for categorization (Update with `tags`) + - Archive instead of delete (see Best Practice below) + +### Archive-First Best Practice (Recommended for AI Agents) + +**Best practice: AI agents should move unwanted content to an "Archive" shelf +instead of deleting it.** A human can then review and permanently delete from +the archive manually. This prevents accidental data loss since delete is +permanent and cascading (deleting a book removes all its chapters and pages). + +Setup: +1. Create a shelf called "Archive" in BookStack (one-time, manual) +2. Optionally create an "Archive" book inside it for orphaned pages/chapters +3. In the n8n AI Agent, either omit the Delete tool entirely, or instruct the + agent via system prompt to prefer archiving over deletion + +How to archive with existing tools: +- **Archive a page**: Update Page with `chapter_id` or `book_id` pointing to + the Archive book/chapter +- **Archive a chapter**: Update Chapter with `book_id` pointing to the Archive book + (all pages inside move with it automatically) +- **Archive a book**: Update the source shelf's `books` list to remove the book, + then update the Archive shelf's `books` list to add it. Note: shelves use + many-to-many relationships, so you need to Get both shelves first to read + their current book lists, then Update each with the modified lists. + +If the Delete tool is provided to the AI agent, it can be used at the agent's +discretion - but be aware that deletes are permanent and cascading. There is no +undo via the API (BookStack has a recycle bin in the web UI, but it is not +accessible via API). + +### Token-Efficient Navigation Strategy + +A BookStack with 1000 books and 100,000 pages would cost millions of tokens if loaded +entirely. The tool descriptions encode the following strategy to prevent this: + +**ALWAYS DO:** +1. **Search first** (Global Search with type filter, limit 5-20) - returns only IDs, names, + and short previews (~50 tokens per result vs ~5000 tokens for full page content) +2. **Get single items by ID** - after Search identifies candidates, fetch only the specific + pages/books/chapters you need to inspect +3. **Use tags** for categorization - enables `{tag:name}` search without scanning content +4. **Navigate top-down** when needed: Get Shelf (lists books) -> Get Book (lists chapters + and pages) -> Get Chapter (lists pages) -> Get Page (full content) + +**NEVER DO:** +- `Get Many Pages` with `Return All = true` - loads ALL pages from the instance +- `Get Many` with high limits (>50) when you only need a few specific items +- `Deep Dive` with `Return All` or high limits - multiplies API calls (N+1 pattern) +- Fetch full content of items you don't need to read + +**Token cost comparison (approximate):** +| Operation | Tokens per item | 1000 items | +|-----------|----------------|------------| +| Search result (preview) | ~50 | ~50,000 | +| Get Many result (no content) | ~100 | ~100,000 | +| Get single page (full content) | ~2,000-50,000 | CATASTROPHIC | +| Deep Dive search result | ~2,000-50,000 | CATASTROPHIC | + +--- + +## Development Commands + +```bash +pnpm install # Install dependencies +pnpm run build # Build TypeScript to dist/ +pnpm run dev # Start local n8n instance with this node +pnpm run lint # Run ESLint +pnpm run format # Check formatting with Prettier +pnpm run lint:fix # Auto-fix lint issues +pnpm run format:fix # Auto-fix formatting +``` + +### Build Output + +`pnpm build` runs `n8n-node build` which: +1. Compiles TypeScript from `nodes/` and `credentials/` to `dist/` +2. Copies static files (icons, JSON metadata) + +The `dist/` directory is what n8n loads at runtime. + +--- + +## Testing Notes + +- No automated test suite exists yet +- Test manually by running `pnpm run dev` and using the node in n8n UI +- Key scenarios to test after changes: + - Create Page/Book/Chapter/Shelf with name (should work as before) + - Create Page/Book/Chapter/Shelf without name (should auto-generate) + - Update Page with different `book_id`/`chapter_id` (should move page) + - Global Search with various filters + - Deep Dive on search results + +--- + +## Known Issues + +See `KNOWN_ISSUES.md` for a full list of issues found during QA audit (24 total, +3 fixed in v1.4.0, 21 remaining). The top 3 most impactful remaining issues: + +1. ~~**Tags don't support name:value pairs**~~ **FIXED in v1.4.0** - Tags now split + on `:` to produce `{name: "topic", value: "networking"}`. +2. **Audit-log sort baked into URL** - `sort=-created_at` is in the URL path string + instead of the query parameters object. Fragile but works. +3. **Attachment/Image limit ignores returnAll** - The `.map()` in these description + files overwrites `displayOptions` instead of merging, so Limit stays visible + when Return All is on. + +--- + +## Common Pitfalls + +1. **`getNodeParameter` fallback**: Always pass `undefined` as third arg for optional fields, + not `''`. Empty string would be treated as a provided value. + +2. **Tags format**: UI accepts comma-separated strings with optional `name:value` pairs + (e.g., `"topic:networking, status:active"`). The `buildRequestBody()` method splits + on the first `:` to produce `{name: "topic", value: "networking"}`. Tags without + a colon become `{name: "tagname"}`. Empty entries from double-commas or whitespace + are filtered out. + +3. **Shelf books format**: UI accepts comma-separated IDs (e.g., "1,5,12"), converted + to `[1, 5, 12]` integer array by `buildRequestBody()`. + +4. **Attachment/Image creation**: These bypass `buildRequestBody()` and `handleCreateOperation()`. + They use dedicated multipart handlers. Changes to the shared create flow do NOT affect them. + +5. **Page requires content**: Unlike books/chapters/shelves, pages MUST have either + `html` or `markdown` on creation. This is enforced by `validatePageCreation()`. + Always use `markdown` unless the user explicitly needs HTML. Never set both fields. + +6. **Moving content**: Pages are moved by setting `book_id` or `chapter_id` on Update. + Chapters are moved by setting `book_id` on Update. Books cannot be moved (they're top-level). + Shelves reference books via the `books` ID array (many-to-many). + +7. **Search type filter**: The node appends `{type:X}` to the search query string. + BookStack type values: `page`, `chapter`, `book`, `bookshelf` (note: "bookshelf", not "shelf"). + +8. **`new Bookstack()` in execute()**: The `execute()` method has `this: IExecuteFunctions` + (n8n binds `this` to the execution context, not the class). All instance methods are + called via `const nodeInstance = new Bookstack()`. This works because all needed state + (`resourceEndpoints`, `resourceFields`) comes from class field declarations, not + constructor parameters. + +9. **Audit-log sort parameter**: The sort is baked into the URL string + (`'/audit-log?sort=-created_at'`). Don't add a separate `sort` to `qs` or it will + conflict. See KNOWN_ISSUES.md #2. + +10. **`decodeHtmlEntities` limitations**: Only used for fallback name generation. + Does not handle hex entities (except `'`), `'`, or case-insensitive + entity names (except ` `). Double-decodes `&lt;` to `<`. Acceptable + for its limited scope but don't reuse for general HTML processing. + +11. **NodeApiError detection**: Uses string comparison (`e?.constructor?.name === 'NodeApiError'`) + which could break under minification. If you need to modify error handling, consider + using `instanceof` with proper imports instead. + +12. **`book_id`/`chapter_id` sent as strings**: These ID fields are `type: 'string'` + in the UI but BookStack API expects integers. PHP/Laravel auto-coerces `"123"` to + `123`, so this works. If BookStack ever adds strict type checking, these would need + conversion to integers in `buildRequestBody()`. + +13. **Search `preview_html` is an object, not a string**: The BookStack search API + returns `preview_html` as `{name: string, content: string}` with HTML-highlighted + matches. The node maps it to `preview` in the output. It works but the shape is + different from what the description implies. + +14. **BookStack also has `description_html` (max 2000 chars)**: Books, chapters, and + shelves accept both `description` (plaintext, max 1900) and `description_html` + (HTML, max 2000). The node only exposes plaintext `description`. This is a feature + gap, not a bug. + +15. **API max pagination `count` is configurable**: CLAUDE.md and the code assume max + 500 per request (`API_MAX_ITEM_COUNT`). This is BookStack's default but instances + can configure it lower. The node paginates correctly regardless. + +--- + +## Version History + +| Version | Key Changes | +|---------|-------------| +| 1.4.0 | Optional name on Create, auto-name generation, AI-optimized descriptions, token-efficient navigation strategy, markdown-first, tag name:value support, CLAUDE.md, KNOWN_ISSUES.md | +| 1.3.0 | Attachment resource support with CRUD and multipart handling | +| 1.2.0 | Image resource support with CRUD and multipart handling | +| 1.1.0 | Switch to pnpm | +| 1.0.0 | Deep Dive in main node, "Return All" pagination, unified search | +| 0.11.x | Audit log, simplified API, multi-Node version matrix | +| 0.10.x | Initial release, BookStack CRUD for books/pages/chapters/shelves | diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md new file mode 100644 index 0000000..d725390 --- /dev/null +++ b/KNOWN_ISSUES.md @@ -0,0 +1,314 @@ +# Known Issues + +Issues found during QA audit (v1.4.0). Items marked **(v1.4.0)** were introduced +by the v1.4.0 changes. All others are pre-existing. + +--- + +## MEDIUM Severity + +### ~~1. Tags do not support BookStack name:value pairs~~ **FIXED in v1.4.0** + +Tags now support `name:value` pairs. Input `"topic:networking"` produces +`{name: "topic", value: "networking"}`. The `{tag:name=value}` search syntax works. + +--- + +### 2. Audit-log endpoint has query parameter baked into the URL path + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 382 + +**Description:** The endpoint string is `'/audit-log?sort=-created_at'` with additional +`qs` parameters (`count`, `offset`) passed separately. The HTTP library constructs: +`baseUrl/audit-log?sort=-created_at&count=50&offset=0` + +While most HTTP libraries handle this correctly by appending with `&`, it is fragile +and unconventional. + +**Suggested Fix:** Move `sort` into the `qs` object: + +```typescript +const qs = { count, offset, sort: '-created_at' } as IDataObject; +const res = await bookstackApiRequest.call(context, 'GET', '/audit-log', {}, qs); +``` + +--- + +### 3. Attachment and Image limit field ignores returnAll condition + +**Files:** +- `nodes/Bookstack/descriptions/Attachment.description.ts`, lines 216-224 +- `nodes/Bookstack/descriptions/Image.description.ts`, lines 123-131 + +**Description:** Both files use `.map()` which completely **replaces** the original +`displayOptions` from `ListOperations.ts`. The `limit` field has +`displayOptions: { show: { returnAll: [false] } }` to hide when "Return All" is enabled. +For attachments and images, this condition is lost. + +Compare with Book/Page/Chapter/Shelf descriptions which correctly use: +```typescript +...(op.displayOptions?.show ?? {}) +``` + +**Impact:** The Limit field remains visible in the UI even when "Return All" is toggled on +for Attachment and Image resources. + +**Suggested Fix:** Use the same spread pattern as the other description files. + +--- + +### 4. handleAuditLogOperation lacks Array.isArray safety check + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 386 + +**Description:** The line casts directly without verifying the result is an array: + +```typescript +const data: JsonObject[] = (res?.data ?? res) as JsonObject[]; +``` + +Compare with `handleGetAllOperation` which safely checks: +```typescript +Array.isArray(data) ? (data as JsonObject[]) : [] +``` + +**Impact:** If the API response shape changes or returns an error object, +`aggregated.push(...data)` would throw a runtime error. + +--- + +## LOW Severity + +### 5. No filename sanitization in multipart upload + +**File:** `nodes/Bookstack/utils/BookstackApiHelpers.ts`, line 101 + +The `fileName` from `binaryData.fileName` is interpolated directly into the +`Content-Disposition` header. If the filename contains double quotes or CRLF +characters, it could break multipart parsing. Unlikely in practice. + +--- + +### 6. No trailing-slash normalization on baseUrl + +**File:** `nodes/Bookstack/utils/BookstackApiHelpers.ts`, line 46 + +If the user configures `baseUrl` as `https://example.com/api/` (with trailing slash), +the resulting URL will have a double slash: `https://example.com/api//pages`. +Most web servers handle this, but a `.replace(/\/+$/, '')` on `baseUrl` would be cleaner. + +--- + +### 7. HTML heading regex does not enforce matching tag levels + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 143 + +The regex `/]*>([\s\S]*?)<\/h[1-6]>/i` would match `

text

` +(mismatched levels). Extremely unlikely with BookStack-generated HTML. Only affects +fallback name generation. + +--- + +### 8. decodeHtmlEntities inconsistent case sensitivity + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 115-123 + +` ` is matched with `/gi` (case-insensitive) but other entities use `/g` +(case-sensitive). So `&NBSP;` decodes but `&` does not. In practice HTML +entities are almost always lowercase. + +--- + +### 9. decodeHtmlEntities does not handle hex numeric entities + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 123 + +Only decimal numeric entities (`A`) are handled. Hex entities like `A` +are not decoded (except the explicitly handled `'`). Acceptable since the +method is only used for fallback name generation. + +--- + +### 10. decodeHtmlEntities missing ' entity + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 114-124 + +Handles `'` and `'` (apostrophe) but not the named entity `'`. +Valid XML/HTML5 but less common in BookStack output. + +--- + +### 11. NodeApiError detection via string comparison + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 753 + +`e?.constructor?.name === 'NodeApiError'` relies on class name surviving minification. +Using `instanceof NodeApiError` (with import) would be safer. + +--- + +### 12. Null error in catch block could cause secondary TypeError + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 756 + +If `error` is `null` (extremely unlikely), `e.message` would throw TypeError. +Using `e?.message` would be more defensive. + +--- + +## INFO Severity + +### 13. tsconfig.json references non-existent .eslintrc.js + +**File:** `tsconfig.json`, line 28 + +The `include` array references `./.eslintrc.js` which does not exist. Silently +ignored by TypeScript but represents stale configuration. + +--- + +### 14. Type aliases add no type safety + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 26-27 + +`SearchItemMinimal` and `ContentResponseShape` are both aliases for `IDataObject`. +Consider using proper interfaces with defined properties. + +--- + +### 15. handleUpdateImageOperation can send empty multipart request + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 576-592 + +If `name` and `binaryPropertyName` are both empty, a multipart form with zero fields +is sent. BookStack API would return a validation error. + +--- + +### 16. Double-decoding potential in decodeHtmlEntities + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 114-124 + +Replacement order means `&lt;` becomes `<` then `<` (double-decoded). +Acceptable for fallback name generation but not faithful HTML entity decoding. + +--- + +### 17. `requiresDataPath: 'single'` on search query field + +**File:** `nodes/Bookstack/descriptions/Global.description.ts`, line 29 + +The `requiresDataPath: 'single'` property on the search query field is unusual for +a plain text search string. This property is typically used for expression-mode fields +that need data path resolution. On a plain search string field, this could cause +unexpected behavior in certain n8n execution contexts. May be intentional for AI +tool usage but warrants verification. + +--- + +### 18. No `continueOnFail()` support in execute() + +**File:** `nodes/Bookstack/Bookstack.node.ts`, lines 747-758 + +The catch block always rethrows errors. It never checks `this.continueOnFail()`, +which is a common n8n pattern for allowing workflows to continue despite errors. +Items that fail will halt the entire node execution. This may be intentional for +data integrity but limits error resilience in batch workflows. + +--- + +### 19. Multipart request handler has no try/catch error wrapping + +**File:** `nodes/Bookstack/utils/BookstackApiHelpers.ts`, line 122 + +The `bookstackApiRequest()` function wraps HTTP errors in `NodeApiError` for clean +error messages. The `bookstackApiRequestMultipart()` function does NOT - errors from +attachment/image uploads propagate as raw, unformatted exceptions. Users see +unfriendly error messages on upload failures. + +**Suggested Fix:** Add the same try/catch pattern: +```typescript +try { + return await this.helpers.httpRequestWithAuthentication.call(this, 'bookstackApi', options); +} catch (error) { + throw new NodeApiError(this.getNode(), error); +} +``` + +--- + +### 20. Redundant credential null checks + +**File:** `nodes/Bookstack/utils/BookstackApiHelpers.ts`, lines 32-35 and 67-70 + +`if (!credentials)` check after `await this.getCredentials('bookstackApi')`. The +`getCredentials()` method already throws if credentials are not found, so these +checks are dead code. Not harmful, just redundant. + +--- + +### ~~21. `!body.name` falsy check is too broad~~ **FIXED in v1.4.0** + +The check now uses `body.name === undefined || body.name === ''` instead of `!body.name`. + +--- + +### 22. Removing `required: true` from name silently accepts empty names **(v1.4.0)** + +**Files:** Page/Book/Chapter/Shelf description files + +Previously, the n8n UI blocked submission when the name field was empty (enforced +by `required: true`). Now the UI allows it and the node auto-generates a fallback +name (e.g. `page-2026-04-08T14-30-00`). Manual users who accidentally leave the +name empty will not get a validation error - they will get an auto-generated name +they may not notice until later. + +This is an intentional design decision for AI agent usage but changes behavior +for manual users. + +--- + +### ~~23. Page ID description is misleading for Delete operation~~ **FIXED in v1.4.0** + +The ID field description now documents all three operations separately: +Get returns fields, Update returns updated object, Delete returns empty on success. + +--- + +### 24. README typo: "lunch" instead of "launch" + +**File:** `README.md`, line 122 + +"To lunch a local instance" should be "To launch a local instance". + +--- + +### 25. `book_id`/`chapter_id`/`default_template_id` sent as strings instead of integers + +**Files:** Page.description.ts, Chapter.description.ts, Book.description.ts + +These ID fields are `type: 'string'` in the n8n field definitions but the BookStack +API expects integers. PHP/Laravel auto-coerces `"123"` to `123` so this works in +practice, but it is technically sending the wrong type. + +--- + +### 26. Search result `preview_html` is an object, not a string + +**File:** `nodes/Bookstack/Bookstack.node.ts`, line 258 + +BookStack returns `preview_html` as `{name: string, content: string}` with +HTML-highlighted matches. The node maps it to `preview` in the output. The +description says "preview (short text snippet)" which is slightly inaccurate. + +--- + +### 27. Missing API fields not exposed by the node + +The following BookStack API fields exist but are not in the node: +- `description_html` (books, chapters, shelves - HTML variant, max 2000 chars) +- `priority` (pages, chapters - ordering within parent) +- `image` / `cover` (books, shelves - cover image) +- `default_template_id` for chapters (only exposed for books currently) + +These are feature gaps for potential future versions. diff --git a/nodes/Bookstack/Bookstack.node.ts b/nodes/Bookstack/Bookstack.node.ts index 14a55a2..af736fd 100644 --- a/nodes/Bookstack/Bookstack.node.ts +++ b/nodes/Bookstack/Bookstack.node.ts @@ -34,7 +34,7 @@ export class Bookstack implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Manage BookStack resources', + description: 'Manage BookStack content. Hierarchy: Shelves → Books → Chapters → Pages. Use Search to find content (returns IDs and previews), then Get by ID for full details. Update can move content by changing parent IDs. Delete is permanent and cascading. Prefer markdown over HTML.', defaults: { name: 'Bookstack' }, inputs: ['main'], outputs: ['main'], @@ -75,7 +75,7 @@ export class Bookstack implements INodeType { private readonly resourceFields: Record = { book: ['name', 'description', 'tags', 'default_template_id'], - page: ['name', 'html', 'markdown', 'book_id', 'chapter_id', 'tags'], + page: ['name', 'page_title', 'html', 'markdown', 'book_id', 'chapter_id', 'tags'], chapter: ['name', 'description', 'book_id', 'tags'], shelf: ['name', 'description', 'books', 'tags'], }; @@ -95,9 +95,29 @@ export class Bookstack implements INodeType { } } - // Convert tags to array format + // If page_title is set (AI-generated), use it as the name and remove the extra field + if (body.page_title && typeof body.page_title === 'string') { + body.name = body.page_title; + delete body.page_title; + } else { + delete body.page_title; + } + + // Convert tags to array format (supports "name" and "name:value" pairs) if (body.tags && typeof body.tags === 'string') { - body.tags = body.tags.split(',').map((t: string) => ({ name: t.trim() })); + body.tags = body.tags + .split(',') + .map((t: string) => { + const trimmed = t.trim(); + if (!trimmed) return null; + const colonIdx = trimmed.indexOf(':'); + if (colonIdx > 0) { + const value = trimmed.slice(colonIdx + 1); + return value ? { name: trimmed.slice(0, colonIdx), value } : { name: trimmed.slice(0, colonIdx) }; + } + return { name: trimmed }; + }) + .filter((tag) => tag !== null); } // Convert books to array of integers @@ -111,6 +131,57 @@ export class Bookstack implements INodeType { return body; } + private static decodeHtmlEntities(text: string): string { + return text + .replace(/ /gi, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code))); + } + + private generateFallbackName(resource: string, body: IDataObject): string { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + + if (resource === 'page') { + // Prefer markdown (token-efficient, AI-friendly) over HTML for name extraction + const markdown = body.markdown as string | undefined; + if (markdown) { + const mdHeadingMatch = markdown.match(/^#{1,6}\s+(.+)$/m); + if (mdHeadingMatch?.[1]?.trim()) return mdHeadingMatch[1].trim().slice(0, 255); + const firstLine = markdown.split(/\n/).find((l: string) => l.trim()); + if (firstLine?.trim()) return firstLine.trim().slice(0, 255); + } + + // HTML fallback: only process a prefix to avoid expensive regex on large documents + const html = body.html as string | undefined; + if (html) { + const htmlPrefix = html.slice(0, 2000); + const headingMatch = htmlPrefix.match(/]*>([\s\S]*?)<\/h[1-6]>/i); + if (headingMatch?.[1]) { + const text = Bookstack.decodeHtmlEntities( + headingMatch[1].replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(), + ); + if (text) return text.slice(0, 255); + } + const textContent = Bookstack.decodeHtmlEntities( + htmlPrefix.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(), + ); + if (textContent) return textContent.slice(0, 255); + } + } + + if (['book', 'chapter', 'shelf'].includes(resource)) { + const desc = body.description as string | undefined; + if (desc?.trim()) return desc.trim().slice(0, 255); + } + + return `${resource}-${timestamp}`; + } + private validatePageCreation( body: IDataObject, context: IExecuteFunctions, @@ -397,6 +468,11 @@ export class Bookstack implements INodeType { this.validatePageCreation(body, context, itemIndex); } + // Auto-generate name if not provided (BookStack API requires it) + if ((body.name === undefined || body.name === '') && this.resourceFields[resource]?.includes('name')) { + body.name = this.generateFallbackName(resource, body); + } + return await bookstackApiRequest.call(context, 'POST', `/${endpoint}`, body, {}); } diff --git a/nodes/Bookstack/descriptions/Attachment.description.ts b/nodes/Bookstack/descriptions/Attachment.description.ts index 440d09e..28faef3 100644 --- a/nodes/Bookstack/descriptions/Attachment.description.ts +++ b/nodes/Bookstack/descriptions/Attachment.description.ts @@ -13,11 +13,11 @@ export const attachmentOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create an attachment' }, + { name: 'Delete', value: 'delete', action: 'Delete an attachment' }, + { name: 'Get', value: 'get', action: 'Get an attachment' }, + { name: 'Get Many', value: 'getAll', action: 'Get many attachments' }, + { name: 'Update', value: 'update', action: 'Update an attachment' }, ], default: 'getAll', }, diff --git a/nodes/Bookstack/descriptions/Book.description.ts b/nodes/Bookstack/descriptions/Book.description.ts index 12a8057..c4ddbdf 100644 --- a/nodes/Bookstack/descriptions/Book.description.ts +++ b/nodes/Bookstack/descriptions/Book.description.ts @@ -13,11 +13,11 @@ export const bookOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create a book' }, + { name: 'Delete', value: 'delete', action: 'Delete a book' }, + { name: 'Get', value: 'get', action: 'Get a book' }, + { name: 'Get Many', value: 'getAll', action: 'Get many books' }, + { name: 'Update', value: 'update', action: 'Update a book' }, ], default: 'getAll', }, @@ -36,14 +36,13 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'The unique identifier of the book', + description: 'Numeric ID of the book. Get returns: id, name, slug, description, created_at, updated_at, created_by, updated_by, owned_by, tags[], and a "contents" array of {id, name, type, pages[]} for chapters and direct pages. Update returns the updated book. Delete returns empty on success.', placeholder: 'Enter book ID (e.g., 123)', }, { displayName: 'Name', name: 'name', type: 'string', - required: true, displayOptions: { show: { resource: ['book'], @@ -51,7 +50,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the book (max 255 characters)', + description: 'Book title (max 255 chars). The AI should generate a concise, descriptive title. If omitted, auto-generated from description or timestamp.', }, { displayName: 'Name', @@ -64,7 +63,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the book (max 255 characters)', + description: 'New title for the book (max 255 chars). Leave empty to keep current name.', }, { displayName: 'Description', @@ -77,7 +76,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Description of the book (max 1900 characters)', + description: 'Short description of the book (max 1900 chars). Shown in listings and search results.', }, { displayName: 'Default Template ID', @@ -90,7 +89,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the default template for pages in this book', + description: 'ID of a page to use as the default template for new pages in this book.', }, { displayName: 'Tags', @@ -103,7 +102,7 @@ export const bookFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags for the book', + description: 'Comma-separated tags. Supports name:value pairs (e.g. "category:devops, status:active"). On update, this REPLACES all existing tags. Searchable via {tag:name} or {tag:name=value} in Global Search.', }, ...listOperations.map((op) => ({ ...op, diff --git a/nodes/Bookstack/descriptions/Chapter.description.ts b/nodes/Bookstack/descriptions/Chapter.description.ts index f3c0021..6a0152e 100644 --- a/nodes/Bookstack/descriptions/Chapter.description.ts +++ b/nodes/Bookstack/descriptions/Chapter.description.ts @@ -13,11 +13,11 @@ export const chapterOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create a chapter' }, + { name: 'Delete', value: 'delete', action: 'Delete a chapter' }, + { name: 'Get', value: 'get', action: 'Get a chapter' }, + { name: 'Get Many', value: 'getAll', action: 'Get many chapters' }, + { name: 'Update', value: 'update', action: 'Update a chapter' }, ], default: 'getAll', }, @@ -36,7 +36,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'The unique identifier of the chapter', + description: 'Numeric ID of the chapter. Get returns: id, name, slug, book_id, description, priority, created_at, updated_at, created_by, updated_by, tags[], and a "pages" array of {id, name, slug, book_id, chapter_id, priority}. Update returns the updated chapter. Delete returns empty on success.', placeholder: 'Enter chapter ID (e.g., 101)', }, { @@ -51,7 +51,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the book this chapter belongs to', + description: 'ID of the book this chapter belongs to.', placeholder: 'Enter book ID', }, { @@ -65,14 +65,13 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the book this chapter belongs to', + description: 'ID of the parent book. Set this to MOVE the chapter to a different book.', placeholder: 'Enter book ID', }, { displayName: 'Name', name: 'name', type: 'string', - required: true, displayOptions: { show: { resource: ['chapter'], @@ -80,7 +79,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the chapter (max 255 characters)', + description: 'Chapter title (max 255 chars). The AI should generate a concise, descriptive title. If omitted, auto-generated from description or timestamp.', placeholder: 'Enter chapter name', }, { @@ -94,7 +93,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the chapter (max 255 characters)', + description: 'New title for the chapter (max 255 chars). Leave empty to keep current name.', placeholder: 'Enter chapter name', }, { @@ -108,7 +107,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Description of the chapter (max 1900 characters)', + description: 'Short description of the chapter (max 1900 chars). Shown in listings and search results.', placeholder: 'Enter chapter description', }, { @@ -122,7 +121,7 @@ export const chapterFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags for the chapter', + description: 'Comma-separated tags. Supports name:value pairs (e.g. "topic:networking, status:reviewed"). On update, this REPLACES all existing tags. Searchable via {tag:name} or {tag:name=value} in Global Search.', placeholder: 'tag1, tag2, tag3', }, ...listOperations.map((op) => ({ diff --git a/nodes/Bookstack/descriptions/Global.description.ts b/nodes/Bookstack/descriptions/Global.description.ts index 00a3b51..5a74b1f 100644 --- a/nodes/Bookstack/descriptions/Global.description.ts +++ b/nodes/Bookstack/descriptions/Global.description.ts @@ -38,7 +38,7 @@ export const globalFields: INodeProperties[] = [ }, placeholder: 'Enter search terms', description: - 'Search query to find content across all resources (books, pages, chapters, shelves)', + 'Search terms to find content. Each result contains: id, name, type (page/book/chapter/bookshelf), url, preview (short text snippet), tags[], book, chapter. Does NOT return full page content - use Get by ID for that. Use the Content Type Filter below instead of adding {type:...} to the query. Additional inline filters: {in_name:text}, {in_body:text}, {tag:tagname}, {created_by:id}, {updated_by:id}. Example: "networking {in_name:setup}". An empty result list means no matches were found.', }, { displayName: 'Content Type Filter', @@ -59,7 +59,7 @@ export const globalFields: INodeProperties[] = [ ], default: 'all', description: - 'Filter search results by content type. This filter will be automatically added to your search query.', + 'Filter by content type. Automatically appended to the query - do NOT also add {type:...} manually. Use "Pages" to find pages, "Books" for books, etc.', }, { displayName: 'Return All', @@ -72,7 +72,7 @@ export const globalFields: INodeProperties[] = [ }, }, default: false, - description: 'Whether to return all results or only up to a given limit', + description: 'WARNING: On large BookStack instances this can return thousands of results. Use a low limit instead and refine your search query. Only enable if you are certain the result set is small.', }, { displayName: 'Limit', @@ -90,8 +90,8 @@ export const globalFields: INodeProperties[] = [ minValue: 1, maxValue: 100, }, - description: 'Max number of results to return', - placeholder: '100', + description: 'Max results to return. Keep this low (5-20) when searching to save tokens. You can always search again with different terms if needed.', + placeholder: '20', }, { displayName: 'Deep Dive', @@ -105,7 +105,7 @@ export const globalFields: INodeProperties[] = [ }, default: false, description: - 'Whether to automatically retrieve full content for all found pages, chapters, books and shelves. This provides more context but may increase execution time.', + 'WARNING: Fetches FULL content for every search result (1 extra API call per result). With 50 results this means 50 additional requests and massive token usage. Only use with a small limit (5-10) when you need to compare content. Prefer: search with low limit, then Get individual pages by ID.', }, { displayName: 'Return All', diff --git a/nodes/Bookstack/descriptions/Image.description.ts b/nodes/Bookstack/descriptions/Image.description.ts index 2089612..61b46ae 100644 --- a/nodes/Bookstack/descriptions/Image.description.ts +++ b/nodes/Bookstack/descriptions/Image.description.ts @@ -13,11 +13,11 @@ export const imageOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create an image' }, + { name: 'Delete', value: 'delete', action: 'Delete an image' }, + { name: 'Get', value: 'get', action: 'Get an image' }, + { name: 'Get Many', value: 'getAll', action: 'Get many images' }, + { name: 'Update', value: 'update', action: 'Update an image' }, ], default: 'getAll', }, diff --git a/nodes/Bookstack/descriptions/ListOperations.ts b/nodes/Bookstack/descriptions/ListOperations.ts index 25266ab..a81b9f5 100644 --- a/nodes/Bookstack/descriptions/ListOperations.ts +++ b/nodes/Bookstack/descriptions/ListOperations.ts @@ -7,7 +7,7 @@ export const listOperations: INodeProperties[] = [ type: 'string', default: '', placeholder: 'name, created_at, updated_at, ...', - description: 'Field to sort by', + description: 'Field name to sort by. Same fields available as for filtering (e.g. id, name, slug, created_at, updated_at, created_by).', }, { displayName: 'Sort Direction', @@ -44,7 +44,7 @@ export const listOperations: INodeProperties[] = [ name: 'field', type: 'string', default: '', - description: 'Field to filter on', + description: 'Field name to filter on. Available fields depend on the resource. Common: id, name, slug, created_at, updated_at, created_by, updated_by, owned_by. Pages also support: book_id, chapter_id, draft, template, priority. Chapters also support: book_id, priority. Attachments/Images also support: uploaded_to.', }, { displayName: 'Operation', @@ -78,7 +78,7 @@ export const listOperations: INodeProperties[] = [ name: 'returnAll', type: 'boolean', default: false, - description: 'Whether to return all results or only up to a given limit', + description: 'WARNING: Fetches ALL items across all pages. On large BookStack instances with thousands of items this wastes massive amounts of tokens. Use Search (Global resource) to find specific items instead. Only enable for small, bounded datasets.', }, { displayName: 'Limit', @@ -88,7 +88,7 @@ export const listOperations: INodeProperties[] = [ minValue: 1, }, default: 50, - description: 'Max number of results to return', + description: 'Max items to return. Keep low (10-20) to save tokens. Use Search (Global resource) with keywords to find specific items instead of listing large numbers.', displayOptions: { show: { returnAll: [false], diff --git a/nodes/Bookstack/descriptions/Page.description.ts b/nodes/Bookstack/descriptions/Page.description.ts index a13ad10..3b8e785 100644 --- a/nodes/Bookstack/descriptions/Page.description.ts +++ b/nodes/Bookstack/descriptions/Page.description.ts @@ -13,11 +13,11 @@ export const pageOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create a page' }, + { name: 'Delete', value: 'delete', action: 'Delete a page' }, + { name: 'Get', value: 'get', action: 'Get a page' }, + { name: 'Get Many', value: 'getAll', action: 'Get many pages' }, + { name: 'Update', value: 'update', action: 'Update a page' }, ], default: 'getAll', }, @@ -36,14 +36,26 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'The unique identifier of the page', + description: 'Numeric ID of the page. Get returns: id, name, slug, html, markdown, book_id, chapter_id, priority, created_at, updated_at, created_by, updated_by, tags[]. Update returns the updated page. Delete returns an empty response on success.', placeholder: 'Enter page ID (e.g., 456)', }, { - displayName: 'Name', - name: 'name', + displayName: 'Page Title (AI)', + name: 'page_title', + type: 'string', + displayOptions: { + show: { + resource: ['page'], + operation: ['create'], + }, + }, + default: '', + description: 'Page title generated by the AI model (max 255 chars). A concise, descriptive title that summarizes the page content. If set, this overrides the Name field below. If both are empty, a title is auto-generated from the content.', + }, + { + displayName: 'Test Name Field', + name: 'test_name_field', type: 'string', - required: true, displayOptions: { show: { resource: ['page'], @@ -51,7 +63,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the page (max 255 characters)', + description: 'TEST: Checking if renaming from "name" fixes the $fromAI button issue.', }, { displayName: 'Name', @@ -64,7 +76,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the page (max 255 characters)', + description: 'New title for the page (max 255 chars). Leave empty to keep current name.', }, { displayName: 'Book ID', @@ -77,7 +89,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the book this page belongs to (required on create if no Chapter ID)', + description: 'ID of the parent book. On create: set EITHER book_id OR chapter_id (not both). On update: set this to MOVE the page to a different book (removes it from its current chapter).', }, { displayName: 'Chapter ID', @@ -90,7 +102,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'ID of the chapter this page belongs to (required on create if no Book ID)', + description: 'ID of the parent chapter. On create: set EITHER chapter_id OR book_id (not both). On update: setting chapter_id alone moves the page into that chapter (no need to clear book_id).', }, { displayName: 'HTML Content', @@ -103,7 +115,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'HTML content of the page (required on create if no Markdown Content)', + description: 'HTML body of the page. Only use if markdown is not suitable - markdown uses ~3x fewer tokens. Required on create if markdown is not set. On update, REPLACES the entire page content. Do NOT set both html and markdown.', }, { displayName: 'Markdown Content', @@ -116,7 +128,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Markdown content of the page (required on create if no HTML Content)', + description: 'Markdown body of the page. PREFERRED over HTML - uses ~3x fewer tokens. Required on create if html is not set. On update, REPLACES the entire page content. To append, first Get the page, merge content, then Update. Do NOT set both html and markdown.', }, { displayName: 'Tags', @@ -129,7 +141,7 @@ export const pageFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags for the page', + description: 'Comma-separated tags. Supports name:value pairs (e.g. "topic:networking, status:reviewed"). On update, this REPLACES all existing tags. Searchable via {tag:name} or {tag:name=value} in Global Search.', }, ...listOperations.map((op) => ({ ...op, diff --git a/nodes/Bookstack/descriptions/Shelf.description.ts b/nodes/Bookstack/descriptions/Shelf.description.ts index 9a7ec2b..0e9cc20 100644 --- a/nodes/Bookstack/descriptions/Shelf.description.ts +++ b/nodes/Bookstack/descriptions/Shelf.description.ts @@ -13,11 +13,11 @@ export const shelfOperations: INodeProperties[] = [ }, }, options: [ - { name: 'Create', value: 'create', action: 'Create' }, - { name: 'Delete', value: 'delete', action: 'Delete' }, - { name: 'Get', value: 'get', action: 'Get' }, - { name: 'Get Many', value: 'getAll', action: 'Get many' }, - { name: 'Update', value: 'update', action: 'Update' }, + { name: 'Create', value: 'create', action: 'Create a shelf' }, + { name: 'Delete', value: 'delete', action: 'Delete a shelf' }, + { name: 'Get', value: 'get', action: 'Get a shelf' }, + { name: 'Get Many', value: 'getAll', action: 'Get many shelves' }, + { name: 'Update', value: 'update', action: 'Update a shelf' }, ], default: 'getAll', }, @@ -36,14 +36,13 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'The unique identifier of the shelf', + description: 'Numeric ID of the shelf. Get returns: id, name, slug, description, created_at, updated_at, created_by, updated_by, tags[], and a "books" array of {id, name, slug}. Update returns the updated shelf. Delete returns empty on success.', placeholder: 'Enter shelf ID (e.g., 789)', }, { displayName: 'Name', name: 'name', type: 'string', - required: true, displayOptions: { show: { resource: ['shelf'], @@ -51,7 +50,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the shelf (max 255 characters)', + description: 'Shelf title (max 255 chars). The AI should generate a concise, descriptive title. If omitted, auto-generated from description or timestamp.', placeholder: 'Enter shelf name', }, { @@ -65,7 +64,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Name of the shelf (max 255 characters)', + description: 'New title for the shelf (max 255 chars). Leave empty to keep current name.', placeholder: 'Enter shelf name', }, { @@ -79,7 +78,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Description of the shelf (max 1900 characters)', + description: 'Short description of the shelf (max 1900 chars). Shown in listings and search results.', placeholder: 'Enter shelf description', }, { @@ -93,7 +92,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated list of book IDs to add to this shelf', + description: 'Comma-separated list of book IDs to assign to this shelf (e.g. "1,5,12"). Replaces the current book list on update.', placeholder: '123,456,789', }, { @@ -107,7 +106,7 @@ export const shelfFields: INodeProperties[] = [ }, }, default: '', - description: 'Comma-separated tags for the shelf', + description: 'Comma-separated tags. Supports name:value pairs (e.g. "area:infrastructure, status:active"). On update, this REPLACES all existing tags. Searchable via {tag:name} or {tag:name=value} in Global Search.', placeholder: 'tag1, tag2, tag3', }, ...listOperations.map((op) => ({ diff --git a/package.json b/package.json index ff17dc8..c86e139 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-bookstack", - "version": "1.3.0", + "version": "1.4.0", "description": "Community n8n node for the BookStack API", "keywords": [ "n8n",