docs : add BookStack connector design spec

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 08:45:30 +01:00
parent 25b648a1b1
commit 2c93e83e6b

View File

@@ -0,0 +1,295 @@
# BookStack Connector — Design Spec
**Date:** 2026-03-15
**BookStack version:** v25.12.8
**Pattern:** Mirror of Gitea connector
## Overview
Connecteur BookStack permettant de lier des documents (pages et livres) du wiki à des tâches Lesstime. Chaque projet peut être associé à une étagère (shelf) BookStack, et les utilisateurs peuvent rechercher et lier des pages/livres de cette étagère à leurs tâches.
## Périmètre
- Types liés : **pages** et **livres** (books)
- Niveau projet : liaison à une **étagère** (shelf)
- Niveau tâche : liaison à une ou plusieurs **pages/livres** de l'étagère du projet
- Recherche : filtrée dans l'étagère du projet uniquement
- Stockage : **référence** (titre + URL), pas d'aperçu du contenu
- Auth BookStack : Token ID + Token Secret (header `Authorization: Token {id}:{secret}`)
## Backend
### Entités
#### BookStackConfiguration (singleton)
```php
// src/Entity/BookStackConfiguration.php
class BookStackConfiguration
{
private ?int $id;
private ?string $url = null;
private ?string $encryptedTokenId = null;
private ?string $encryptedTokenSecret = null;
public function hasToken(): bool; // vérifie que les deux sont présents
}
```
- Chiffrement via `TokenEncryptor` existant (même pattern que Gitea)
- Repository avec `findSingleton()`
#### TaskBookStackLink
```php
// src/Entity/TaskBookStackLink.php
class TaskBookStackLink
{
private ?int $id;
private Task $task; // ManyToOne, CASCADE on delete
private int $bookstackId; // ID dans BookStack
private string $bookstackType; // 'page' | 'book'
private string $title; // titre au moment du lien (cache)
private string $url; // URL complète
private \DateTimeImmutable $createdAt;
}
```
#### Project (extension)
Ajout de deux champs :
- `bookstackShelfId` (nullable int)
- `bookstackShelfName` (nullable string) — cache du nom pour affichage
### Service
#### BookStackApiService
```php
// src/Service/BookStackApiService.php
class BookStackApiService
{
public function testConnection(): bool;
public function listShelves(): array;
public function searchInShelf(int $shelfId, string $query): array;
public function getPage(int $id): array;
public function getBook(int $id): array;
}
```
- Utilise `HttpClientInterface` (Symfony HttpClient)
- Auth : header `Authorization: Token {tokenId}:{tokenSecret}`
- Timeout : 10 secondes
- `testConnection()` : GET `/api/docs.json`
- `listShelves()` : GET `/api/shelves` (paginé, récupère toutes les pages)
- `searchInShelf()` : GET `/api/search?query={query}+{type:page|book}` puis filtre côté serveur par shelf ID
- `getPage()` : GET `/api/pages/{id}`
- `getBook()` : GET `/api/books/{id}`
#### BookStackApiException
```php
// src/Exception/BookStackApiException.php
class BookStackApiException extends \RuntimeException {}
```
### API Resources & Endpoints
#### Admin
| Méthode | Route | Ressource API Platform | Sécurité |
|---------|-------|----------------------|----------|
| GET | `/api/settings/bookstack` | BookStackSettings | ROLE_ADMIN |
| PUT | `/api/settings/bookstack` | BookStackSettings | ROLE_ADMIN |
| POST | `/api/settings/bookstack/test` | BookStackTestConnection | ROLE_ADMIN |
**BookStackSettings** (DTO) :
- Read : `url`, `hasToken`
- Write : `url`, `tokenId`, `tokenSecret`
**BookStackTestConnection** (DTO) :
- Read : `success`
#### Projet
| Méthode | Route | Ressource API Platform | Sécurité |
|---------|-------|----------------------|----------|
| GET | `/api/bookstack/shelves` | BookStackShelf | ROLE_ADMIN |
**BookStackShelf** (DTO) :
- Read : `id`, `name`
L'étagère sélectionnée est sauvée via le PATCH existant de Project (`bookstackShelfId`, `bookstackShelfName`).
#### Tâche
| Méthode | Route | Ressource API Platform | Sécurité |
|---------|-------|----------------------|----------|
| GET | `/api/tasks/{taskId}/bookstack/links` | BookStackLink | Authenticated |
| POST | `/api/tasks/{taskId}/bookstack/links` | BookStackLink | Authenticated |
| DELETE | `/api/tasks/{taskId}/bookstack/links/{id}` | BookStackLink | Authenticated |
| GET | `/api/tasks/{taskId}/bookstack/search?q=` | BookStackSearchResult | Authenticated |
**BookStackLink** (DTO) :
- Read : `id`, `bookstackId`, `bookstackType`, `title`, `url`, `createdAt`
- Write : `bookstackId`, `bookstackType`, `title`, `url`
**BookStackSearchResult** (DTO) :
- Read : `id`, `type`, `name`, `url`
### State Providers / Processors
| Classe | Rôle |
|--------|------|
| `BookStackSettingsProvider` | Lit config singleton, retourne DTO masqué |
| `BookStackSettingsProcessor` | Persiste config, chiffre tokens |
| `BookStackTestConnectionProvider` | Appelle `testConnection()` |
| `BookStackShelfProvider` | Appelle `listShelves()`, mappe en DTOs |
| `BookStackLinkProvider` | Lit `TaskBookStackLink` par task ID |
| `BookStackLinkProcessor` | POST : crée lien en DB / DELETE : supprime |
| `BookStackSearchResultProvider` | Appelle `searchInShelf()`, mappe en DTOs |
### Migration
```sql
CREATE TABLE bookstack_configuration (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
url VARCHAR(255) DEFAULT NULL,
encrypted_token_id TEXT DEFAULT NULL,
encrypted_token_secret TEXT DEFAULT NULL
);
CREATE TABLE task_bookstack_link (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
task_id INT NOT NULL REFERENCES task(id) ON DELETE CASCADE,
bookstack_id INT NOT NULL,
bookstack_type VARCHAR(10) NOT NULL,
title VARCHAR(255) NOT NULL,
url VARCHAR(500) NOT NULL,
created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL
);
CREATE INDEX IDX_task_bookstack_link_task_id ON task_bookstack_link (task_id);
ALTER TABLE project ADD bookstack_shelf_id INT DEFAULT NULL;
ALTER TABLE project ADD bookstack_shelf_name VARCHAR(255) DEFAULT NULL;
```
### Variable d'environnement
Réutilise la même clé de chiffrement que Gitea (`TokenEncryptor` existant) — pas besoin d'une nouvelle clé.
## Frontend
### Service
```typescript
// frontend/services/bookstack.ts
export function useBookStackService() {
// Admin
async function getSettings(): Promise<BookStackSettings>
async function saveSettings(payload: BookStackSettingsWrite): Promise<BookStackSettings>
async function testConnection(): Promise<BookStackTestResult>
// Projet
async function listShelves(): Promise<BookStackShelf[]>
// Tâche
async function getLinks(taskId: number): Promise<BookStackLink[]>
async function addLink(taskId: number, payload: BookStackLinkCreate): Promise<BookStackLink>
async function removeLink(taskId: number, linkId: number): Promise<void>
async function search(taskId: number, query: string): Promise<BookStackSearchResult[]>
}
```
### DTOs
```typescript
// frontend/services/dto/bookstack.ts
type BookStackSettings = { url: string | null; hasToken: boolean }
type BookStackSettingsWrite = { url: string | null; tokenId: string | null; tokenSecret: string | null }
type BookStackTestResult = { success: boolean }
type BookStackShelf = { id: number; name: string }
type BookStackLink = { id: number; bookstackId: number; bookstackType: 'page' | 'book'; title: string; url: string; createdAt: string }
type BookStackLinkCreate = { bookstackId: number; bookstackType: 'page' | 'book'; title: string; url: string }
type BookStackSearchResult = { id: number; type: 'page' | 'book'; name: string; url: string }
```
### Composants
#### AdminBookStackTab.vue
Onglet admin (même pattern que `AdminGiteaTab.vue`) :
- Champs : URL, Token ID, Token Secret
- Bouton "Tester la connexion" avec indicateur résultat
- Indicateur "Token configuré" (ne montre jamais le token)
- Sauvegarde via `saveSettings()`
#### ProjectDrawer.vue (extension)
- Si BookStack est configuré : select pour choisir une étagère
- Charge `listShelves()` à l'ouverture
- Sauvegarde `bookstackShelfId` + `bookstackShelfName` sur le projet via PATCH
#### TaskBookStackLinks.vue
Petit composant intégré dans `TaskModal.vue`, visible directement :
- **Input de recherche** avec debounce (~300ms) → appel `search(taskId, query)` → dropdown résultats
- Chaque résultat : icône (page 📄 / livre 📕) + titre — clic pour ajouter
- **Liste des liens** sous le champ recherche : icône type + titre cliquable (ouvre BookStack dans nouvel onglet) + bouton × supprimer
- Affiché uniquement si le projet de la tâche a une shelf BookStack configurée
- Charge les liens existants au mount via `getLinks(taskId)`
#### TaskModal.vue (extension)
- Ajoute `<TaskBookStackLinks>` dans le modal, conditionné par `project.bookstackShelfId`
- Passe `taskId` et `projectId` en props
## Fichiers à créer/modifier
### Backend — Nouveaux fichiers
```
src/Entity/BookStackConfiguration.php
src/Entity/TaskBookStackLink.php
src/Repository/BookStackConfigurationRepository.php
src/Repository/TaskBookStackLinkRepository.php
src/Service/BookStackApiService.php
src/Exception/BookStackApiException.php
src/ApiResource/BookStackSettings.php
src/ApiResource/BookStackTestConnection.php
src/ApiResource/BookStackShelf.php
src/ApiResource/BookStackLink.php
src/ApiResource/BookStackSearchResult.php
src/State/BookStackSettingsProvider.php
src/State/BookStackSettingsProcessor.php
src/State/BookStackTestConnectionProvider.php
src/State/BookStackShelfProvider.php
src/State/BookStackLinkProvider.php
src/State/BookStackLinkProcessor.php
src/State/BookStackSearchResultProvider.php
migrations/VersionXXXX.php
```
### Backend — Fichiers modifiés
```
src/Entity/Project.php (ajout bookstackShelfId, bookstackShelfName)
```
### Frontend — Nouveaux fichiers
```
frontend/services/bookstack.ts
frontend/services/dto/bookstack.ts
frontend/components/admin/AdminBookStackTab.vue
frontend/components/task/TaskBookStackLinks.vue
```
### Frontend — Fichiers modifiés
```
frontend/components/task/TaskModal.vue (ajout TaskBookStackLinks)
frontend/components/project/ProjectDrawer.vue (ajout select étagère)
frontend/components/admin/ (ajout onglet BookStack dans la page admin)
```