feat(bookstack-api-5gi): implement Attachments CRUD
Add AttachmentsService with List, Get, Create, Update, Delete. Support link attachments via JSON. Add Attachment type and request types. Register service on Client. Add tests.
This commit is contained in:
parent
04cf1565fa
commit
256361e90b
6 changed files with 219 additions and 12 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"worktree_root": "/home/oli/Dev/bookstack-api",
|
"worktree_root": "/home/oli/Dev/bookstack-api",
|
||||||
"last_export_commit": "13a952355eeaa36f1662ff0d861ce8e13eb50fe1",
|
"last_export_commit": "970699afe28e58200abdef9c3e8334dec919e640",
|
||||||
"last_export_time": "2026-01-30T09:53:44.426529458+01:00",
|
"last_export_time": "2026-01-30T09:54:09.809167697+01:00",
|
||||||
"jsonl_hash": "cbbea13985d023f48b36fc95076e6e87fb4918ae5124c895ca0eaad739bf8ae1"
|
"jsonl_hash": "bfab1aefaece07cacb3867cadceb934c1d7e035ea5ff652f6ac66f7143c4d75d"
|
||||||
}
|
}
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
{"id":"bookstack-api-1us","title":"Write README with Quick-Start guide","description":"Create comprehensive README.md with usage documentation.\n\n## Requirements\nFrom PRD Section 8 (Definition of Done):\n- README mit Quick-Start\n\n## Content\n1. **Overview** - What the library does\n2. **Installation** - go get command\n3. **Quick Start** - Basic usage example\n4. **Authentication** - How to get and use API tokens\n5. **Usage Examples**\n - List books\n - Search documentation\n - Get page content\n - Export page\n6. **API Reference** - Link to GoDoc\n7. **License**\n\n## Example Code\n```go\nclient := bookstack.NewClient(bookstack.Config{\n BaseURL: \"https://docs.example.com\",\n TokenID: os.Getenv(\"BOOKSTACK_TOKEN_ID\"),\n TokenSecret: os.Getenv(\"BOOKSTACK_TOKEN_SECRET\"),\n})\n\n// Search documentation\nresults, err := client.Search.Search(ctx, \"deployment\", nil)\n\n// Get a page\npage, err := client.Pages.Get(ctx, 123)\n```\n\n## Acceptance Criteria\n- [ ] Clear installation instructions\n- [ ] Working quick-start example\n- [ ] All main features documented\n- [ ] Links to full documentation","status":"open","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:40:24.187495378+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:40:24.187495378+01:00"}
|
{"id":"bookstack-api-1us","title":"Write README with Quick-Start guide","description":"Create comprehensive README.md with usage documentation.\n\n## Requirements\nFrom PRD Section 8 (Definition of Done):\n- README mit Quick-Start\n\n## Content\n1. **Overview** - What the library does\n2. **Installation** - go get command\n3. **Quick Start** - Basic usage example\n4. **Authentication** - How to get and use API tokens\n5. **Usage Examples**\n - List books\n - Search documentation\n - Get page content\n - Export page\n6. **API Reference** - Link to GoDoc\n7. **License**\n\n## Example Code\n```go\nclient := bookstack.NewClient(bookstack.Config{\n BaseURL: \"https://docs.example.com\",\n TokenID: os.Getenv(\"BOOKSTACK_TOKEN_ID\"),\n TokenSecret: os.Getenv(\"BOOKSTACK_TOKEN_SECRET\"),\n})\n\n// Search documentation\nresults, err := client.Search.Search(ctx, \"deployment\", nil)\n\n// Get a page\npage, err := client.Pages.Get(ctx, 123)\n```\n\n## Acceptance Criteria\n- [ ] Clear installation instructions\n- [ ] Working quick-start example\n- [ ] All main features documented\n- [ ] Links to full documentation","status":"open","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:40:24.187495378+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:40:24.187495378+01:00"}
|
||||||
{"id":"bookstack-api-2x5","title":"Implement SearchService","description":"Implement the SearchService for full-text search across Bookstack content.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| All | GET /api/search?query=... | Full-text search |\n\n## API Methods\n```go\ntype SearchService struct {\n client *Client\n}\n\ntype SearchOptions struct {\n Query string\n // Pagination options\n Count int\n Offset int\n}\n\nfunc (s *SearchService) Search(ctx context.Context, query string, opts *SearchOptions) ([]*SearchResult, error)\n```\n\n## SearchResult\nReturns results across all types:\n- page\n- chapter\n- book\n- bookshelf\n\nEach result includes ID, Name, Slug, Type, URL, Preview.\n\n## User Story\nUS1: Als AI-Agent möchte ich die Bookstack-Dokumentation durchsuchen können, um relevante Seiten für Benutzeranfragen zu finden.\n\n## Acceptance Criteria\n- [ ] SearchService struct created\n- [ ] Search() accepts query string and options\n- [ ] Results include all content types\n- [ ] Proper error handling\n- [ ] Unit tests with mock server","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:31.430412917+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:50:44.683680188+01:00","closed_at":"2026-01-30T09:50:44.683680188+01:00","close_reason":"Closed"}
|
{"id":"bookstack-api-2x5","title":"Implement SearchService","description":"Implement the SearchService for full-text search across Bookstack content.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| All | GET /api/search?query=... | Full-text search |\n\n## API Methods\n```go\ntype SearchService struct {\n client *Client\n}\n\ntype SearchOptions struct {\n Query string\n // Pagination options\n Count int\n Offset int\n}\n\nfunc (s *SearchService) Search(ctx context.Context, query string, opts *SearchOptions) ([]*SearchResult, error)\n```\n\n## SearchResult\nReturns results across all types:\n- page\n- chapter\n- book\n- bookshelf\n\nEach result includes ID, Name, Slug, Type, URL, Preview.\n\n## User Story\nUS1: Als AI-Agent möchte ich die Bookstack-Dokumentation durchsuchen können, um relevante Seiten für Benutzeranfragen zu finden.\n\n## Acceptance Criteria\n- [ ] SearchService struct created\n- [ ] Search() accepts query string and options\n- [ ] Results include all content types\n- [ ] Proper error handling\n- [ ] Unit tests with mock server","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:31.430412917+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:50:44.683680188+01:00","closed_at":"2026-01-30T09:50:44.683680188+01:00","close_reason":"Closed"}
|
||||||
{"id":"bookstack-api-42g","title":"Implement ShelvesService (List, Get)","description":"Implement the ShelvesService with List and Get operations.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| List | GET /api/shelves | All shelves |\n| Get | GET /api/shelves/{id} | Single shelf |\n\n## API Methods\n```go\ntype ShelvesService struct {\n client *Client\n}\n\nfunc (s *ShelvesService) List(ctx context.Context, opts *ListOptions) ([]*Shelf, error)\nfunc (s *ShelvesService) Get(ctx context.Context, id int) (*Shelf, error)\nfunc (s *ShelvesService) ListAll(ctx context.Context) iter.Seq2[*Shelf, error]\n```\n\n## Shelf Fields\n- ID, Name, Slug, Description, CreatedAt, UpdatedAt\n\n## Bookstack Hierarchy\nShelf is the top-level container: Shelf -\u003e Book -\u003e Chapter -\u003e Page\n\n## Acceptance Criteria\n- [ ] ShelvesService struct created\n- [ ] List() returns paginated shelves\n- [ ] Get() returns single shelf by ID\n- [ ] ListAll() iterator implemented\n- [ ] Proper error handling\n- [ ] Unit tests with mock server","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:53.490673653+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:53:07.163294684+01:00","closed_at":"2026-01-30T09:53:07.163294684+01:00","close_reason":"Closed"}
|
{"id":"bookstack-api-42g","title":"Implement ShelvesService (List, Get)","description":"Implement the ShelvesService with List and Get operations.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| List | GET /api/shelves | All shelves |\n| Get | GET /api/shelves/{id} | Single shelf |\n\n## API Methods\n```go\ntype ShelvesService struct {\n client *Client\n}\n\nfunc (s *ShelvesService) List(ctx context.Context, opts *ListOptions) ([]*Shelf, error)\nfunc (s *ShelvesService) Get(ctx context.Context, id int) (*Shelf, error)\nfunc (s *ShelvesService) ListAll(ctx context.Context) iter.Seq2[*Shelf, error]\n```\n\n## Shelf Fields\n- ID, Name, Slug, Description, CreatedAt, UpdatedAt\n\n## Bookstack Hierarchy\nShelf is the top-level container: Shelf -\u003e Book -\u003e Chapter -\u003e Page\n\n## Acceptance Criteria\n- [ ] ShelvesService struct created\n- [ ] List() returns paginated shelves\n- [ ] Get() returns single shelf by ID\n- [ ] ListAll() iterator implemented\n- [ ] Proper error handling\n- [ ] Unit tests with mock server","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:53.490673653+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:53:07.163294684+01:00","closed_at":"2026-01-30T09:53:07.163294684+01:00","close_reason":"Closed"}
|
||||||
{"id":"bookstack-api-5gi","title":"Implement Attachments CRUD","description":"Implement CRUD operations for Attachments.\n\n## Requirements\nFrom PRD feature table: Attachments: CRUD (P2, v0.4)\n\n## API Endpoints (typical Bookstack pattern)\n- GET /api/attachments - List attachments\n- GET /api/attachments/{id} - Get attachment\n- POST /api/attachments - Create attachment\n- PUT /api/attachments/{id} - Update attachment\n- DELETE /api/attachments/{id} - Delete attachment\n\n## API Methods\n```go\ntype AttachmentsService struct {\n client *Client\n}\n\ntype Attachment struct {\n ID int `json:\"id\"`\n Name string `json:\"name\"`\n Extension string `json:\"extension\"`\n PageID int `json:\"uploaded_to\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n}\n\nfunc (s *AttachmentsService) List(ctx context.Context, opts *ListOptions) ([]*Attachment, error)\nfunc (s *AttachmentsService) Get(ctx context.Context, id int) (*Attachment, error)\nfunc (s *AttachmentsService) Create(ctx context.Context, req *AttachmentCreateRequest) (*Attachment, error)\nfunc (s *AttachmentsService) Update(ctx context.Context, id int, req *AttachmentUpdateRequest) (*Attachment, error)\nfunc (s *AttachmentsService) Delete(ctx context.Context, id int) error\n```\n\n## Technical Details\n- May need multipart/form-data for file uploads\n- Consider file size limits\n\n## Acceptance Criteria\n- [ ] Attachment type defined\n- [ ] AttachmentsService with all CRUD methods\n- [ ] File upload handling\n- [ ] Unit tests with mock server","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:54.242422591+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:39:54.242422591+01:00"}
|
{"id":"bookstack-api-5gi","title":"Implement Attachments CRUD","description":"Implement CRUD operations for Attachments.\n\n## Requirements\nFrom PRD feature table: Attachments: CRUD (P2, v0.4)\n\n## API Endpoints (typical Bookstack pattern)\n- GET /api/attachments - List attachments\n- GET /api/attachments/{id} - Get attachment\n- POST /api/attachments - Create attachment\n- PUT /api/attachments/{id} - Update attachment\n- DELETE /api/attachments/{id} - Delete attachment\n\n## API Methods\n```go\ntype AttachmentsService struct {\n client *Client\n}\n\ntype Attachment struct {\n ID int `json:\"id\"`\n Name string `json:\"name\"`\n Extension string `json:\"extension\"`\n PageID int `json:\"uploaded_to\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n}\n\nfunc (s *AttachmentsService) List(ctx context.Context, opts *ListOptions) ([]*Attachment, error)\nfunc (s *AttachmentsService) Get(ctx context.Context, id int) (*Attachment, error)\nfunc (s *AttachmentsService) Create(ctx context.Context, req *AttachmentCreateRequest) (*Attachment, error)\nfunc (s *AttachmentsService) Update(ctx context.Context, id int, req *AttachmentUpdateRequest) (*Attachment, error)\nfunc (s *AttachmentsService) Delete(ctx context.Context, id int) error\n```\n\n## Technical Details\n- May need multipart/form-data for file uploads\n- Consider file size limits\n\n## Acceptance Criteria\n- [ ] Attachment type defined\n- [ ] AttachmentsService with all CRUD methods\n- [ ] File upload handling\n- [ ] Unit tests with mock server","status":"in_progress","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:54.242422591+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:54:14.75565358+01:00","comments":[{"id":5,"issue_id":"bookstack-api-5gi","author":"Oliver Jakoubek","text":"Plan: (1) Add Attachment type to types.go, (2) Create attachments.go with AttachmentsService, (3) For Create - Bookstack supports both file upload (multipart) and link attachments (JSON). Will implement link attachments via JSON for now since file upload requires multipart which is more complex. (4) List/Get/Update/Delete follow standard pattern. (5) Register service in bookstack.go Client struct.","created_at":"2026-01-30T08:54:20Z"}]}
|
||||||
{"id":"bookstack-api-7qx","title":"Implement Comments CRUD","description":"Implement CRUD operations for Comments.\n\n## Requirements\nFrom PRD feature table: Comments: CRUD (P3, v0.5)\n\n## API Endpoints (typical Bookstack pattern)\n- GET /api/comments - List comments\n- GET /api/comments/{id} - Get comment\n- POST /api/comments - Create comment\n- PUT /api/comments/{id} - Update comment\n- DELETE /api/comments/{id} - Delete comment\n\n## API Methods\n```go\ntype CommentsService struct {\n client *Client\n}\n\ntype Comment struct {\n ID int `json:\"id\"`\n PageID int `json:\"page_id\"`\n ParentID int `json:\"parent_id,omitempty\"`\n HTML string `json:\"html\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n CreatedBy int `json:\"created_by\"`\n}\n\nfunc (s *CommentsService) List(ctx context.Context, opts *ListOptions) ([]*Comment, error)\nfunc (s *CommentsService) Get(ctx context.Context, id int) (*Comment, error)\nfunc (s *CommentsService) Create(ctx context.Context, req *CommentCreateRequest) (*Comment, error)\nfunc (s *CommentsService) Update(ctx context.Context, id int, req *CommentUpdateRequest) (*Comment, error)\nfunc (s *CommentsService) Delete(ctx context.Context, id int) error\n```\n\n## Technical Details\n- Comments are attached to pages\n- Support nested comments (parent_id)\n\n## Acceptance Criteria\n- [ ] Comment type defined\n- [ ] CommentsService with all CRUD methods\n- [ ] Support for page filtering\n- [ ] Unit tests with mock server","status":"open","priority":3,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:40:23.920608941+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:40:23.920608941+01:00"}
|
{"id":"bookstack-api-7qx","title":"Implement Comments CRUD","description":"Implement CRUD operations for Comments.\n\n## Requirements\nFrom PRD feature table: Comments: CRUD (P3, v0.5)\n\n## API Endpoints (typical Bookstack pattern)\n- GET /api/comments - List comments\n- GET /api/comments/{id} - Get comment\n- POST /api/comments - Create comment\n- PUT /api/comments/{id} - Update comment\n- DELETE /api/comments/{id} - Delete comment\n\n## API Methods\n```go\ntype CommentsService struct {\n client *Client\n}\n\ntype Comment struct {\n ID int `json:\"id\"`\n PageID int `json:\"page_id\"`\n ParentID int `json:\"parent_id,omitempty\"`\n HTML string `json:\"html\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n CreatedBy int `json:\"created_by\"`\n}\n\nfunc (s *CommentsService) List(ctx context.Context, opts *ListOptions) ([]*Comment, error)\nfunc (s *CommentsService) Get(ctx context.Context, id int) (*Comment, error)\nfunc (s *CommentsService) Create(ctx context.Context, req *CommentCreateRequest) (*Comment, error)\nfunc (s *CommentsService) Update(ctx context.Context, id int, req *CommentUpdateRequest) (*Comment, error)\nfunc (s *CommentsService) Delete(ctx context.Context, id int) error\n```\n\n## Technical Details\n- Comments are attached to pages\n- Support nested comments (parent_id)\n\n## Acceptance Criteria\n- [ ] Comment type defined\n- [ ] CommentsService with all CRUD methods\n- [ ] Support for page filtering\n- [ ] Unit tests with mock server","status":"open","priority":3,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:40:23.920608941+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:40:23.920608941+01:00"}
|
||||||
{"id":"bookstack-api-8ea","title":"Implement HTTP helper and request building","description":"Create internal HTTP helper for making authenticated API requests.\n\n## Requirements\nImplement the internal `do` method on Client:\n\n```go\nfunc (c *Client) do(ctx context.Context, method, path string, body, result any) error {\n // 1. Build request\n // 2. Set Auth header: Authorization: Token \u003cid\u003e:\u003csecret\u003e\n // 3. Execute request\n // 4. Check response status\n // 5. On error: return APIError\n // 6. On success: unmarshal JSON into result\n}\n```\n\n## Technical Details\n- Support GET, POST, PUT, DELETE methods\n- JSON content type for request/response\n- Context support for cancellation\n- Proper URL joining (BaseURL + path)\n\n## Acceptance Criteria\n- [ ] do() method implemented\n- [ ] Auth header correctly formatted\n- [ ] JSON marshaling/unmarshaling works\n- [ ] Context cancellation supported\n- [ ] Unit tests with httptest.Server","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:06.247263859+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:48:02.200483913+01:00","closed_at":"2026-01-30T09:48:02.200483913+01:00","close_reason":"Closed","comments":[{"id":1,"issue_id":"bookstack-api-8ea","author":"Oliver Jakoubek","text":"Plan: Implement http.go with do() method combining buildRequest+doRequest. Will implement: (1) do() as single entry point, (2) buildRequest with auth header + JSON body, (3) response handling with APIError parsing. Tests with httptest.Server. Keep existing ListOptions.","created_at":"2026-01-30T08:47:25Z"}]}
|
{"id":"bookstack-api-8ea","title":"Implement HTTP helper and request building","description":"Create internal HTTP helper for making authenticated API requests.\n\n## Requirements\nImplement the internal `do` method on Client:\n\n```go\nfunc (c *Client) do(ctx context.Context, method, path string, body, result any) error {\n // 1. Build request\n // 2. Set Auth header: Authorization: Token \u003cid\u003e:\u003csecret\u003e\n // 3. Execute request\n // 4. Check response status\n // 5. On error: return APIError\n // 6. On success: unmarshal JSON into result\n}\n```\n\n## Technical Details\n- Support GET, POST, PUT, DELETE methods\n- JSON content type for request/response\n- Context support for cancellation\n- Proper URL joining (BaseURL + path)\n\n## Acceptance Criteria\n- [ ] do() method implemented\n- [ ] Auth header correctly formatted\n- [ ] JSON marshaling/unmarshaling works\n- [ ] Context cancellation supported\n- [ ] Unit tests with httptest.Server","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:06.247263859+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:48:02.200483913+01:00","closed_at":"2026-01-30T09:48:02.200483913+01:00","close_reason":"Closed","comments":[{"id":1,"issue_id":"bookstack-api-8ea","author":"Oliver Jakoubek","text":"Plan: Implement http.go with do() method combining buildRequest+doRequest. Will implement: (1) do() as single entry point, (2) buildRequest with auth header + JSON body, (3) response handling with APIError parsing. Tests with httptest.Server. Keep existing ListOptions.","created_at":"2026-01-30T08:47:25Z"}]}
|
||||||
{"id":"bookstack-api-8op","title":"Implement Client and Config structs","description":"Create the main Client struct and Config for client initialization with token-based authentication.\n\n## Requirements\nFrom PRD Section 5 (API \u0026 Interface-Spezifikation):\n\n```go\ntype Config struct {\n BaseURL string // e.g. \"https://docs.jakoubek.net\"\n TokenID string // API Token ID\n TokenSecret string // API Token Secret\n HTTPClient *http.Client // optional, for tests/mocking\n}\n\ntype Client struct {\n Books *BooksService\n Pages *PagesService\n Chapters *ChaptersService\n Shelves *ShelvesService\n Search *SearchService\n}\n\nfunc NewClient(cfg Config) *Client\n```\n\n## Authentication\nHeader format: `Authorization: Token \u003ctoken_id\u003e:\u003ctoken_secret\u003e`\n\n## Acceptance Criteria\n- [ ] Config struct with BaseURL, TokenID, TokenSecret, optional HTTPClient\n- [ ] Client struct with service fields (initially nil)\n- [ ] NewClient() constructor validates required fields\n- [ ] Default http.Client used when not provided\n- [ ] Unit tests for NewClient()","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:06.037277135+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:45:31.33966902+01:00","closed_at":"2026-01-30T09:45:31.33966902+01:00","close_reason":"Closed"}
|
{"id":"bookstack-api-8op","title":"Implement Client and Config structs","description":"Create the main Client struct and Config for client initialization with token-based authentication.\n\n## Requirements\nFrom PRD Section 5 (API \u0026 Interface-Spezifikation):\n\n```go\ntype Config struct {\n BaseURL string // e.g. \"https://docs.jakoubek.net\"\n TokenID string // API Token ID\n TokenSecret string // API Token Secret\n HTTPClient *http.Client // optional, for tests/mocking\n}\n\ntype Client struct {\n Books *BooksService\n Pages *PagesService\n Chapters *ChaptersService\n Shelves *ShelvesService\n Search *SearchService\n}\n\nfunc NewClient(cfg Config) *Client\n```\n\n## Authentication\nHeader format: `Authorization: Token \u003ctoken_id\u003e:\u003ctoken_secret\u003e`\n\n## Acceptance Criteria\n- [ ] Config struct with BaseURL, TokenID, TokenSecret, optional HTTPClient\n- [ ] Client struct with service fields (initially nil)\n- [ ] NewClient() constructor validates required fields\n- [ ] Default http.Client used when not provided\n- [ ] Unit tests for NewClient()","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:06.037277135+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:45:31.33966902+01:00","closed_at":"2026-01-30T09:45:31.33966902+01:00","close_reason":"Closed"}
|
||||||
{"id":"bookstack-api-9at","title":"Implement Pages Delete","description":"Implement delete operation for Pages.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| Delete | DELETE /api/pages/{id} | Delete page |\n\n## API Method\n```go\nfunc (s *PagesService) Delete(ctx context.Context, id int) error\n```\n\n## Technical Details\n- Returns no content on success\n- 404 if page not found\n- May require appropriate permissions\n\n## Acceptance Criteria\n- [ ] Delete() removes page by ID\n- [ ] Returns nil on success\n- [ ] Proper error handling (404 -\u003e ErrNotFound, 403 -\u003e ErrForbidden)\n- [ ] Unit tests with mock server","status":"in_progress","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:53.980583894+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:53:48.547019309+01:00"}
|
{"id":"bookstack-api-9at","title":"Implement Pages Delete","description":"Implement delete operation for Pages.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| Delete | DELETE /api/pages/{id} | Delete page |\n\n## API Method\n```go\nfunc (s *PagesService) Delete(ctx context.Context, id int) error\n```\n\n## Technical Details\n- Returns no content on success\n- 404 if page not found\n- May require appropriate permissions\n\n## Acceptance Criteria\n- [ ] Delete() removes page by ID\n- [ ] Returns nil on success\n- [ ] Proper error handling (404 -\u003e ErrNotFound, 403 -\u003e ErrForbidden)\n- [ ] Unit tests with mock server","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:53.980583894+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:54:09.877149269+01:00","closed_at":"2026-01-30T09:54:09.877149269+01:00","close_reason":"Closed"}
|
||||||
{"id":"bookstack-api-9xo","title":"Implement BooksService (List, Get)","description":"Implement the BooksService with List and Get operations.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| List | GET /api/books | All books |\n| Get | GET /api/books/{id} | Single book |\n\n## API Methods\n```go\ntype BooksService struct {\n client *Client\n}\n\nfunc (s *BooksService) List(ctx context.Context, opts *ListOptions) ([]*Book, error)\nfunc (s *BooksService) Get(ctx context.Context, id int) (*Book, error)\n```\n\n## ListOptions\nSupport pagination parameters:\n- count (max 500)\n- offset\n- sort (+name, -created_at, etc.)\n- filter[field]\n\n## Acceptance Criteria\n- [ ] BooksService struct created\n- [ ] List() returns paginated books\n- [ ] Get() returns single book by ID\n- [ ] Proper error handling (404 -\u003e ErrNotFound)\n- [ ] Unit tests with mock server","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:30.949469353+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:49:50.19693007+01:00","closed_at":"2026-01-30T09:49:50.19693007+01:00","close_reason":"Closed","comments":[{"id":4,"issue_id":"bookstack-api-9xo","author":"Oliver Jakoubek","text":"Plan: (1) Add listResponse generic wrapper and queryString builder to http.go, (2) Implement List/Get in books.go, (3) Add tests with httptest mock server.","created_at":"2026-01-30T08:49:15Z"}]}
|
{"id":"bookstack-api-9xo","title":"Implement BooksService (List, Get)","description":"Implement the BooksService with List and Get operations.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| List | GET /api/books | All books |\n| Get | GET /api/books/{id} | Single book |\n\n## API Methods\n```go\ntype BooksService struct {\n client *Client\n}\n\nfunc (s *BooksService) List(ctx context.Context, opts *ListOptions) ([]*Book, error)\nfunc (s *BooksService) Get(ctx context.Context, id int) (*Book, error)\n```\n\n## ListOptions\nSupport pagination parameters:\n- count (max 500)\n- offset\n- sort (+name, -created_at, etc.)\n- filter[field]\n\n## Acceptance Criteria\n- [ ] BooksService struct created\n- [ ] List() returns paginated books\n- [ ] Get() returns single book by ID\n- [ ] Proper error handling (404 -\u003e ErrNotFound)\n- [ ] Unit tests with mock server","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:30.949469353+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:49:50.19693007+01:00","closed_at":"2026-01-30T09:49:50.19693007+01:00","close_reason":"Closed","comments":[{"id":4,"issue_id":"bookstack-api-9xo","author":"Oliver Jakoubek","text":"Plan: (1) Add listResponse generic wrapper and queryString builder to http.go, (2) Implement List/Get in books.go, (3) Add tests with httptest mock server.","created_at":"2026-01-30T08:49:15Z"}]}
|
||||||
{"id":"bookstack-api-adp","title":"Set up Go module and project structure","description":"Initialize the Go module and create the basic project structure as defined in the PRD.\n\n## Requirements\n- Initialize go.mod with module name\n- Create placeholder files for the flat package structure:\n - bookstack.go (Client, Config, NewClient)\n - types.go (data structures)\n - errors.go (error types)\n - http.go (HTTP helpers)\n- Set up .gitignore for Go projects\n\n## Technical Details\n- Go 1.21+ required\n- Zero external dependencies (standard library only)\n- Module should be publishable as standalone Go module\n\n## Acceptance Criteria\n- [ ] go.mod exists with proper module name\n- [ ] Basic file structure created\n- [ ] `go build ./...` succeeds\n- [ ] .gitignore includes Go-specific patterns","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:05.818311718+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:38:24.407997076+01:00","closed_at":"2026-01-30T09:38:24.407997076+01:00","close_reason":"Closed"}
|
{"id":"bookstack-api-adp","title":"Set up Go module and project structure","description":"Initialize the Go module and create the basic project structure as defined in the PRD.\n\n## Requirements\n- Initialize go.mod with module name\n- Create placeholder files for the flat package structure:\n - bookstack.go (Client, Config, NewClient)\n - types.go (data structures)\n - errors.go (error types)\n - http.go (HTTP helpers)\n- Set up .gitignore for Go projects\n\n## Technical Details\n- Go 1.21+ required\n- Zero external dependencies (standard library only)\n- Module should be publishable as standalone Go module\n\n## Acceptance Criteria\n- [ ] go.mod exists with proper module name\n- [ ] Basic file structure created\n- [ ] `go build ./...` succeeds\n- [ ] .gitignore includes Go-specific patterns","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:05.818311718+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:38:24.407997076+01:00","closed_at":"2026-01-30T09:38:24.407997076+01:00","close_reason":"Closed"}
|
||||||
{"id":"bookstack-api-bu8","title":"Add GoDoc documentation for all public APIs","description":"Ensure all exported types and functions have proper GoDoc comments.\n\n## Requirements\nFrom PRD Section 8 (Definition of Done):\n- Alle public APIs dokumentiert (GoDoc)\n\n## Documentation Standards\n- Package-level doc comment in bookstack.go\n- All exported types documented\n- All exported functions documented\n- All exported constants/variables documented\n- Include usage examples in doc comments where helpful\n\n## Example\n```go\n// Client is the Bookstack API client. Use NewClient to create a new instance.\n// Client is safe for concurrent use.\ntype Client struct {\n // Books provides access to the Books API.\n Books *BooksService\n ...\n}\n\n// NewClient creates a new Bookstack API client with the provided configuration.\n// BaseURL, TokenID, and TokenSecret are required.\n//\n// Example:\n//\n// client := bookstack.NewClient(bookstack.Config{\n// BaseURL: \"https://docs.example.com\",\n// TokenID: \"token-id\",\n// TokenSecret: \"token-secret\",\n// })\nfunc NewClient(cfg Config) *Client\n```\n\n## Acceptance Criteria\n- [ ] Package documentation present\n- [ ] All exported types documented\n- [ ] All exported functions documented\n- [ ] Documentation renders correctly in pkg.go.dev style\n- [ ] No golint documentation warnings","status":"open","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:40:24.752365504+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:40:24.752365504+01:00"}
|
{"id":"bookstack-api-bu8","title":"Add GoDoc documentation for all public APIs","description":"Ensure all exported types and functions have proper GoDoc comments.\n\n## Requirements\nFrom PRD Section 8 (Definition of Done):\n- Alle public APIs dokumentiert (GoDoc)\n\n## Documentation Standards\n- Package-level doc comment in bookstack.go\n- All exported types documented\n- All exported functions documented\n- All exported constants/variables documented\n- Include usage examples in doc comments where helpful\n\n## Example\n```go\n// Client is the Bookstack API client. Use NewClient to create a new instance.\n// Client is safe for concurrent use.\ntype Client struct {\n // Books provides access to the Books API.\n Books *BooksService\n ...\n}\n\n// NewClient creates a new Bookstack API client with the provided configuration.\n// BaseURL, TokenID, and TokenSecret are required.\n//\n// Example:\n//\n// client := bookstack.NewClient(bookstack.Config{\n// BaseURL: \"https://docs.example.com\",\n// TokenID: \"token-id\",\n// TokenSecret: \"token-secret\",\n// })\nfunc NewClient(cfg Config) *Client\n```\n\n## Acceptance Criteria\n- [ ] Package documentation present\n- [ ] All exported types documented\n- [ ] All exported functions documented\n- [ ] Documentation renders correctly in pkg.go.dev style\n- [ ] No golint documentation warnings","status":"open","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:40:24.752365504+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:40:24.752365504+01:00"}
|
||||||
|
|
|
||||||
56
attachments.go
Normal file
56
attachments.go
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
package bookstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AttachmentsService handles operations on attachments.
|
||||||
|
type AttachmentsService struct {
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a list of attachments with optional filtering.
|
||||||
|
func (s *AttachmentsService) List(ctx context.Context, opts *ListOptions) ([]Attachment, error) {
|
||||||
|
var resp listResponse[Attachment]
|
||||||
|
err := s.client.do(ctx, "GET", "/api/attachments"+opts.queryString(), nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a single attachment by ID.
|
||||||
|
func (s *AttachmentsService) Get(ctx context.Context, id int) (*Attachment, error) {
|
||||||
|
var a Attachment
|
||||||
|
err := s.client.do(ctx, "GET", fmt.Sprintf("/api/attachments/%d", id), nil, &a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new link attachment.
|
||||||
|
func (s *AttachmentsService) Create(ctx context.Context, req *AttachmentCreateRequest) (*Attachment, error) {
|
||||||
|
var a Attachment
|
||||||
|
err := s.client.do(ctx, "POST", "/api/attachments", req, &a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates an existing attachment.
|
||||||
|
func (s *AttachmentsService) Update(ctx context.Context, id int, req *AttachmentUpdateRequest) (*Attachment, error) {
|
||||||
|
var a Attachment
|
||||||
|
err := s.client.do(ctx, "PUT", fmt.Sprintf("/api/attachments/%d", id), req, &a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes an attachment by ID.
|
||||||
|
func (s *AttachmentsService) Delete(ctx context.Context, id int) error {
|
||||||
|
return s.client.do(ctx, "DELETE", fmt.Sprintf("/api/attachments/%d", id), nil, nil)
|
||||||
|
}
|
||||||
121
attachments_test.go
Normal file
121
attachments_test.go
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
package bookstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAttachmentsService_List(t *testing.T) {
|
||||||
|
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/api/attachments" {
|
||||||
|
t.Errorf("path = %s, want /api/attachments", r.URL.Path)
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"data": []map[string]any{{"id": 1, "name": "file.pdf", "uploaded_to": 5}},
|
||||||
|
"total": 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
attachments, err := c.Attachments.List(context.Background(), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(attachments) != 1 {
|
||||||
|
t.Fatalf("got %d, want 1", len(attachments))
|
||||||
|
}
|
||||||
|
if attachments[0].Name != "file.pdf" {
|
||||||
|
t.Errorf("Name = %q", attachments[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttachmentsService_Get(t *testing.T) {
|
||||||
|
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"id": 1, "name": "file.pdf", "content": "https://example.com/file.pdf",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
a, err := c.Attachments.Get(context.Background(), 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if a.Content != "https://example.com/file.pdf" {
|
||||||
|
t.Errorf("Content = %q", a.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttachmentsService_Create(t *testing.T) {
|
||||||
|
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
t.Errorf("method = %s, want POST", r.Method)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"id": 2, "name": "link.pdf", "external": true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
a, err := c.Attachments.Create(context.Background(), &AttachmentCreateRequest{
|
||||||
|
Name: "link.pdf",
|
||||||
|
UploadedTo: 5,
|
||||||
|
Link: "https://example.com/link.pdf",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if a.ID != 2 {
|
||||||
|
t.Errorf("ID = %d, want 2", a.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttachmentsService_Update(t *testing.T) {
|
||||||
|
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "PUT" {
|
||||||
|
t.Errorf("method = %s, want PUT", r.Method)
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"id": 1, "name": "renamed.pdf",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
a, err := c.Attachments.Update(context.Background(), 1, &AttachmentUpdateRequest{
|
||||||
|
Name: "renamed.pdf",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if a.Name != "renamed.pdf" {
|
||||||
|
t.Errorf("Name = %q", a.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttachmentsService_Delete(t *testing.T) {
|
||||||
|
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "DELETE" {
|
||||||
|
t.Errorf("method = %s, want DELETE", r.Method)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
err := c.Attachments.Delete(context.Background(), 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttachmentsService_Get_NotFound(t *testing.T) {
|
||||||
|
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"error": map[string]string{"message": "Not found"},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := c.Attachments.Get(context.Background(), 999)
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("expected ErrNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
bookstack.go
16
bookstack.go
|
|
@ -31,11 +31,12 @@ type Client struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
|
||||||
// Service instances
|
// Service instances
|
||||||
Books *BooksService
|
Attachments *AttachmentsService
|
||||||
Pages *PagesService
|
Books *BooksService
|
||||||
Chapters *ChaptersService
|
Chapters *ChaptersService
|
||||||
Shelves *ShelvesService
|
Pages *PagesService
|
||||||
Search *SearchService
|
Search *SearchService
|
||||||
|
Shelves *ShelvesService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new Bookstack API client.
|
// NewClient creates a new Bookstack API client.
|
||||||
|
|
@ -70,11 +71,12 @@ func NewClient(cfg Config) (*Client, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
|
c.Attachments = &AttachmentsService{client: c}
|
||||||
c.Books = &BooksService{client: c}
|
c.Books = &BooksService{client: c}
|
||||||
c.Pages = &PagesService{client: c}
|
|
||||||
c.Chapters = &ChaptersService{client: c}
|
c.Chapters = &ChaptersService{client: c}
|
||||||
c.Shelves = &ShelvesService{client: c}
|
c.Pages = &PagesService{client: c}
|
||||||
c.Search = &SearchService{client: c}
|
c.Search = &SearchService{client: c}
|
||||||
|
c.Shelves = &ShelvesService{client: c}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
types.go
28
types.go
|
|
@ -79,6 +79,34 @@ type PageUpdateRequest struct {
|
||||||
Markdown string `json:"markdown,omitempty"`
|
Markdown string `json:"markdown,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attachment represents a Bookstack attachment.
|
||||||
|
type Attachment struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Extension string `json:"extension"`
|
||||||
|
UploadedTo int `json:"uploaded_to"`
|
||||||
|
External bool `json:"external"`
|
||||||
|
Order int `json:"order"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
CreatedBy int `json:"created_by"`
|
||||||
|
UpdatedBy int `json:"updated_by"`
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachmentCreateRequest contains fields for creating an attachment.
|
||||||
|
type AttachmentCreateRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
UploadedTo int `json:"uploaded_to"`
|
||||||
|
Link string `json:"link,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachmentUpdateRequest contains fields for updating an attachment.
|
||||||
|
type AttachmentUpdateRequest struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Link string `json:"link,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// SearchResult represents a search result from Bookstack.
|
// SearchResult represents a search result from Bookstack.
|
||||||
type SearchResult struct {
|
type SearchResult struct {
|
||||||
Type string `json:"type"` // "page", "chapter", "book", or "shelf"
|
Type string `json:"type"` // "page", "chapter", "book", or "shelf"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue