Files
Lesstime/docs/superpowers/specs/2026-03-15-bookstack-connector-design.md
matthieu bfffbe7041 docs : add BookStack connector implementation plan
21-task plan covering backend (entities, migration, service, API
resources) and frontend (DTOs, service, admin tab, project drawer,
task modal integration). Reviewed and fixed: readonly class issue,
page URL construction, Delete provider handling, task:read group,
search query syntax, security attributes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:00:34 +01:00

12 KiB
Raw Permalink Blame History

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)

// 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()
// 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

// 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é via count/offset, pas page/limit — spécificité BookStack)
  • searchInShelf() : algorithme en 3 étapes :
    1. GET /api/shelves/{shelfId} → récupère la liste des books de l'étagère (IDs)
    2. GET /api/search?query={query} {type:page|book} → recherche globale (espace entre query et filtre type, BookStack syntax)
    3. Filtre côté PHP : pour les books, vérifie que book.id est dans la liste de l'étagère ; pour les pages, vérifie que page.book_id est dans la liste. Exclut les résultats chapter et bookshelf.
    • Note : la liste des books de l'étagère peut être cachée en mémoire pour la durée de la requête.
  • getPage() : GET /api/pages/{id}
  • getBook() : GET /api/books/{id}

BookStackApiException

// 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

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);
CREATE UNIQUE INDEX UNIQ_task_bookstack_link ON task_bookstack_link (task_id, bookstack_id, bookstack_type);

ALTER TABLE project ADD bookstack_shelf_id INT DEFAULT NULL;
ALTER TABLE project ADD bookstack_shelf_name VARCHAR(255) DEFAULT NULL;

Variable d'environnement

Prérequis : renommer GITEA_ENCRYPTION_KEY en ENCRYPTION_KEY (générique) dans TokenEncryptor, .env, et docker/.env.docker. Mettre à jour le message d'erreur dans TokenEncryptor. Cela permet de réutiliser le même service pour chiffrer les tokens BookStack (deux appels encrypt()/decrypt() : un pour tokenId, un pour tokenSecret).

Notes techniques

  • BookStackTestConnectionProvider implémente à la fois ProviderInterface et ProcessorInterface (même pattern que GiteaTestConnectionProvider)
  • Les endpoints collection du frontend utilisent extractHydraMembers() pour extraire les résultats des réponses Hydra
  • Les titres/URLs stockés dans TaskBookStackLink sont des snapshots au moment du lien — pas de rafraîchissement automatique (intentionnel)
  • Le select étagère dans ProjectDrawer n'est affiché que pour les admins (endpoint ROLE_ADMIN)

Frontend

Service

// 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

// 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)
src/Service/TokenEncryptor.php  (renommage GITEA_ENCRYPTION_KEY → ENCRYPTION_KEY)

Config — Fichiers modifiés

.env  (renommage GITEA_ENCRYPTION_KEY → ENCRYPTION_KEY)

Note : docker/.env.docker ne contient pas GITEA_ENCRYPTION_KEY. Les développeurs utilisant docker/.env.docker.local doivent le mettre à jour manuellement.

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)