Initial commit - LoreMind project

This commit is contained in:
2026-04-19 12:08:16 +02:00
parent 95928b7165
commit 094c759f2c
213 changed files with 25358 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
package com.loremind;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Classe principale de l'application LoreMind.
* Point d'entrée Spring Boot qui démarre l'application.
*/
@SpringBootApplication
public class LoreMindApplication {
public static void main(String[] args) {
SpringApplication.run(LoreMindApplication.class, args);
}
}

View File

@@ -0,0 +1,76 @@
package com.loremind.application.campaigncontext;
import com.loremind.domain.campaigncontext.Arc;
import com.loremind.domain.campaigncontext.ports.ArcRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* Service d'application pour le contexte Arc.
* Orchestre la logique métier en utilisant le Port ArcRepository.
* Fait partie de la couche Application de l'Architecture Hexagonale.
*/
@Service
public class ArcService {
private final ArcRepository arcRepository;
public ArcService(ArcRepository arcRepository) {
this.arcRepository = arcRepository;
}
public Arc createArc(String name, String description, String campaignId, int order) {
Arc arc = Arc.builder()
.name(name)
.description(description)
.campaignId(campaignId)
.order(order)
.build();
return arcRepository.save(arc);
}
public Optional<Arc> getArcById(String id) {
return arcRepository.findById(id);
}
public List<Arc> getAllArcs() {
return arcRepository.findAll();
}
public List<Arc> getArcsByCampaignId(String campaignId) {
return arcRepository.findByCampaignId(campaignId);
}
/**
* Met à jour un Arc avec tous ses champs narratifs.
* Accepte un objet Arc pour éviter l'explosion de paramètres (Parameter Object pattern).
*/
public Arc updateArc(String id, Arc updated) {
Optional<Arc> existingArc = arcRepository.findById(id);
if (existingArc.isEmpty()) {
throw new IllegalArgumentException("Arc non trouvé avec l'ID: " + id);
}
Arc arc = existingArc.get();
arc.setName(updated.getName());
arc.setDescription(updated.getDescription());
arc.setOrder(updated.getOrder());
arc.setThemes(updated.getThemes());
arc.setStakes(updated.getStakes());
arc.setGmNotes(updated.getGmNotes());
arc.setRewards(updated.getRewards());
arc.setResolution(updated.getResolution());
arc.setRelatedPageIds(updated.getRelatedPageIds());
return arcRepository.save(arc);
}
public void deleteArc(String id) {
arcRepository.deleteById(id);
}
public boolean arcExists(String id) {
return arcRepository.existsById(id);
}
}

View File

@@ -0,0 +1,84 @@
package com.loremind.application.campaigncontext;
import com.loremind.domain.campaigncontext.Campaign;
import com.loremind.domain.campaigncontext.ports.CampaignRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* Service d'application pour le contexte Campaign.
* Orchestre la logique métier en utilisant le Port CampaignRepository.
* Fait partie de la couche Application de l'Architecture Hexagonale.
*/
@Service
public class CampaignService {
private final CampaignRepository campaignRepository;
public CampaignService(CampaignRepository campaignRepository) {
this.campaignRepository = campaignRepository;
}
/**
* Parameter Object pour la création / mise à jour d'une Campaign.
* Évite une signature à rallonge et rend les évolutions futures (theme,
* coverImageUrl, etc.) sans casser les appelants.
*
* <p>{@code loreId} est nullable : une campagne peut exister sans univers associé.</p>
*/
public record CampaignData(String name, String description, String loreId) {}
public Campaign createCampaign(CampaignData data) {
Campaign campaign = Campaign.builder()
.name(data.name())
.description(data.description())
.loreId(normalizeLoreId(data.loreId()))
.arcsCount(0)
.build();
return campaignRepository.save(campaign);
}
public Optional<Campaign> getCampaignById(String id) {
return campaignRepository.findById(id);
}
public List<Campaign> getAllCampaigns() {
return campaignRepository.findAll();
}
public Campaign updateCampaign(String id, CampaignData data) {
Optional<Campaign> existingCampaign = campaignRepository.findById(id);
if (existingCampaign.isEmpty()) {
throw new IllegalArgumentException("Campaign non trouvé avec l'ID: " + id);
}
Campaign campaign = existingCampaign.get();
campaign.setName(data.name());
campaign.setDescription(data.description());
campaign.setLoreId(normalizeLoreId(data.loreId()));
return campaignRepository.save(campaign);
}
/**
* Normalise un loreId entrant : une chaîne vide/blanche est traitée comme "pas de lien".
* Utile car les payloads JSON peuvent envoyer "" au lieu de null.
*/
private String normalizeLoreId(String loreId) {
return (loreId == null || loreId.isBlank()) ? null : loreId;
}
public void deleteCampaign(String id) {
campaignRepository.deleteById(id);
}
public boolean campaignExists(String id) {
return campaignRepository.existsById(id);
}
public List<Campaign> searchCampaigns(String query) {
if (query == null || query.isBlank()) return List.of();
return campaignRepository.searchByName(query.trim());
}
}

View File

@@ -0,0 +1,73 @@
package com.loremind.application.campaigncontext;
import com.loremind.domain.campaigncontext.Chapter;
import com.loremind.domain.campaigncontext.ports.ChapterRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* Service d'application pour le contexte Chapter.
* Orchestre la logique métier en utilisant le Port ChapterRepository.
* Fait partie de la couche Application de l'Architecture Hexagonale.
*/
@Service
public class ChapterService {
private final ChapterRepository chapterRepository;
public ChapterService(ChapterRepository chapterRepository) {
this.chapterRepository = chapterRepository;
}
public Chapter createChapter(String name, String description, String arcId, int order) {
Chapter chapter = Chapter.builder()
.name(name)
.description(description)
.arcId(arcId)
.order(order)
.build();
return chapterRepository.save(chapter);
}
public Optional<Chapter> getChapterById(String id) {
return chapterRepository.findById(id);
}
public List<Chapter> getAllChapters() {
return chapterRepository.findAll();
}
public List<Chapter> getChaptersByArcId(String arcId) {
return chapterRepository.findByArcId(arcId);
}
/**
* Met à jour un Chapter avec tous ses champs narratifs (Parameter Object pattern).
*/
public Chapter updateChapter(String id, Chapter updated) {
Optional<Chapter> existingChapter = chapterRepository.findById(id);
if (existingChapter.isEmpty()) {
throw new IllegalArgumentException("Chapter non trouvé avec l'ID: " + id);
}
Chapter chapter = existingChapter.get();
chapter.setName(updated.getName());
chapter.setDescription(updated.getDescription());
chapter.setOrder(updated.getOrder());
chapter.setGmNotes(updated.getGmNotes());
chapter.setPlayerObjectives(updated.getPlayerObjectives());
chapter.setNarrativeStakes(updated.getNarrativeStakes());
chapter.setRelatedPageIds(updated.getRelatedPageIds());
return chapterRepository.save(chapter);
}
public void deleteChapter(String id) {
chapterRepository.deleteById(id);
}
public boolean chapterExists(String id) {
return chapterRepository.existsById(id);
}
}

View File

@@ -0,0 +1,78 @@
package com.loremind.application.campaigncontext;
import com.loremind.domain.campaigncontext.Scene;
import com.loremind.domain.campaigncontext.ports.SceneRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* Service d'application pour le contexte Scene.
* Orchestre la logique métier en utilisant le Port SceneRepository.
* Fait partie de la couche Application de l'Architecture Hexagonale.
*/
@Service
public class SceneService {
private final SceneRepository sceneRepository;
public SceneService(SceneRepository sceneRepository) {
this.sceneRepository = sceneRepository;
}
public Scene createScene(String name, String description, String chapterId, int order) {
Scene scene = Scene.builder()
.name(name)
.description(description)
.chapterId(chapterId)
.order(order)
.build();
return sceneRepository.save(scene);
}
public Optional<Scene> getSceneById(String id) {
return sceneRepository.findById(id);
}
public List<Scene> getAllScenes() {
return sceneRepository.findAll();
}
public List<Scene> getScenesByChapterId(String chapterId) {
return sceneRepository.findByChapterId(chapterId);
}
/**
* Met à jour une Scene avec tous ses champs narratifs (Parameter Object pattern).
*/
public Scene updateScene(String id, Scene updated) {
Optional<Scene> existingScene = sceneRepository.findById(id);
if (existingScene.isEmpty()) {
throw new IllegalArgumentException("Scene non trouvée avec l'ID: " + id);
}
Scene scene = existingScene.get();
scene.setName(updated.getName());
scene.setDescription(updated.getDescription());
scene.setOrder(updated.getOrder());
scene.setLocation(updated.getLocation());
scene.setTiming(updated.getTiming());
scene.setAtmosphere(updated.getAtmosphere());
scene.setPlayerNarration(updated.getPlayerNarration());
scene.setGmSecretNotes(updated.getGmSecretNotes());
scene.setChoicesConsequences(updated.getChoicesConsequences());
scene.setCombatDifficulty(updated.getCombatDifficulty());
scene.setEnemies(updated.getEnemies());
scene.setRelatedPageIds(updated.getRelatedPageIds());
return sceneRepository.save(scene);
}
public void deleteScene(String id) {
sceneRepository.deleteById(id);
}
public boolean sceneExists(String id) {
return sceneRepository.existsById(id);
}
}

View File

@@ -0,0 +1,78 @@
package com.loremind.application.lorecontext;
import com.loremind.domain.lorecontext.LoreNode;
import com.loremind.domain.lorecontext.ports.LoreNodeRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* Service d'application pour le contexte LoreNode.
* Orchestre la logique métier en utilisant le Port LoreNodeRepository.
* Fait partie de la couche Application de l'Architecture Hexagonale.
*/
@Service
public class LoreNodeService {
private final LoreNodeRepository loreNodeRepository;
public LoreNodeService(LoreNodeRepository loreNodeRepository) {
this.loreNodeRepository = loreNodeRepository;
}
/**
* Crée un LoreNode (dossier) à partir d'un "objet changes" porteur des valeurs
* souhaitées (pattern Parameter Object) : évite les signatures qui gonflent
* à chaque ajout de champ.
*/
public LoreNode createLoreNode(LoreNode changes) {
LoreNode loreNode = LoreNode.builder()
.name(changes.getName())
.icon(changes.getIcon())
.parentId(changes.getParentId())
.loreId(changes.getLoreId())
.build();
return loreNodeRepository.save(loreNode);
}
public Optional<LoreNode> getLoreNodeById(String id) {
return loreNodeRepository.findById(id);
}
public List<LoreNode> getAllLoreNodes() {
return loreNodeRepository.findAll();
}
public List<LoreNode> getLoreNodesByLoreId(String loreId) {
return loreNodeRepository.findByLoreId(loreId);
}
public List<LoreNode> getLoreNodesByParentId(String parentId) {
return loreNodeRepository.findByParentId(parentId);
}
public List<LoreNode> searchLoreNodes(String query) {
if (query == null || query.isBlank()) return List.of();
return loreNodeRepository.searchByName(query.trim());
}
public LoreNode updateLoreNode(String id, LoreNode changes) {
LoreNode existing = loreNodeRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("LoreNode non trouvé avec l'ID: " + id));
existing.setName(changes.getName());
existing.setIcon(changes.getIcon());
existing.setParentId(changes.getParentId());
// loreId volontairement immuable (un dossier ne migre pas d'un Lore à l'autre).
return loreNodeRepository.save(existing);
}
public void deleteLoreNode(String id) {
loreNodeRepository.deleteById(id);
}
public boolean loreNodeExists(String id) {
return loreNodeRepository.existsById(id);
}
}

View File

@@ -0,0 +1,93 @@
package com.loremind.application.lorecontext;
import com.loremind.domain.lorecontext.Lore;
import com.loremind.domain.lorecontext.ports.LoreNodeRepository;
import com.loremind.domain.lorecontext.ports.LoreRepository;
import com.loremind.domain.lorecontext.ports.PageRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Service d'application pour le contexte Lore.
* Orchestre la logique métier en utilisant le Port LoreRepository.
* Fait partie de la couche Application de l'Architecture Hexagonale.
*
* Note: les compteurs nodeCount/pageCount sont calculés à la volée via
* countByLoreId sur les ports LoreNode et Page, plutôt que stockés en BDD
* (la colonne existe encore mais n'est plus fiable). Source of truth = les
* tables nodes/pages elles-mêmes, jamais de désync possible.
*/
@Service
public class LoreService {
private final LoreRepository loreRepository;
private final LoreNodeRepository loreNodeRepository;
private final PageRepository pageRepository;
public LoreService(LoreRepository loreRepository,
LoreNodeRepository loreNodeRepository,
PageRepository pageRepository) {
this.loreRepository = loreRepository;
this.loreNodeRepository = loreNodeRepository;
this.pageRepository = pageRepository;
}
public Lore createLore(String name, String description) {
Lore lore = Lore.builder()
.name(name)
.description(description)
.nodeCount(0)
.pageCount(0)
.build();
return loreRepository.save(lore);
}
public Optional<Lore> getLoreById(String id) {
return loreRepository.findById(id).map(this::withCounts);
}
public List<Lore> getAllLores() {
return loreRepository.findAll().stream()
.map(this::withCounts)
.collect(Collectors.toList());
}
/**
* Enrichit un Lore avec les compteurs calculés à la volée.
* N+1 acceptable à l'échelle actuelle (quelques dizaines de Lores max).
*/
private Lore withCounts(Lore lore) {
lore.setNodeCount((int) loreNodeRepository.countByLoreId(lore.getId()));
lore.setPageCount((int) pageRepository.countByLoreId(lore.getId()));
return lore;
}
/** Recherche par nom (ILIKE). Résultats sans compteurs — pas utile pour la command palette. */
public List<Lore> searchLores(String query) {
if (query == null || query.isBlank()) return List.of();
return loreRepository.searchByName(query.trim());
}
public Lore updateLore(String id, String name, String description) {
Optional<Lore> existingLore = loreRepository.findById(id);
if (existingLore.isEmpty()) {
throw new IllegalArgumentException("Lore non trouvé avec l'ID: " + id);
}
Lore lore = existingLore.get();
lore.setName(name);
lore.setDescription(description);
return loreRepository.save(lore);
}
public void deleteLore(String id) {
loreRepository.deleteById(id);
}
public boolean loreExists(String id) {
return loreRepository.existsById(id);
}
}

View File

@@ -0,0 +1,95 @@
package com.loremind.application.lorecontext;
import com.loremind.domain.lorecontext.Page;
import com.loremind.domain.lorecontext.ports.PageRepository;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
/**
* Service d'application pour le contexte Page.
* Orchestre la logique métier via le Port PageRepository.
* Couche Application de l'Architecture Hexagonale.
*/
@Service
public class PageService {
private final PageRepository pageRepository;
public PageService(PageRepository pageRepository) {
this.pageRepository = pageRepository;
}
/** Création MVP : seuls les champs structurels sont requis. Le contenu est
* enrichi plus tard depuis l'écran d'édition. */
public Page createPage(String loreId, String nodeId, String templateId, String title) {
Page page = Page.builder()
.loreId(loreId)
.nodeId(nodeId)
.templateId(templateId)
.title(title)
.values(new HashMap<>())
.tags(new ArrayList<>())
.relatedPageIds(new ArrayList<>())
.build();
return pageRepository.save(page);
}
public Optional<Page> getPageById(String id) {
return pageRepository.findById(id);
}
public List<Page> getAllPages() {
return pageRepository.findAll();
}
public List<Page> getPagesByLoreId(String loreId) {
return pageRepository.findByLoreId(loreId);
}
public List<Page> getPagesByNodeId(String nodeId) {
return pageRepository.findByNodeId(nodeId);
}
public List<Page> searchPages(String query) {
if (query == null || query.isBlank()) return List.of();
return pageRepository.searchByTitle(query.trim());
}
/**
* Met à jour une page existante.
* Parameter Object pattern : le controller construit une Page "changes"
* avec les nouvelles valeurs, le service recharge et applique les champs modifiables.
* Les champs structurels immuables sont : id, loreId, templateId.
* Le nodeId est mutable (déplacement d'une page d'un noeud à l'autre).
*/
public Page updatePage(String id, Page changes) {
Page existing = pageRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Page non trouvée avec l'ID: " + id));
existing.setTitle(changes.getTitle());
existing.setNodeId(changes.getNodeId());
existing.setValues(changes.getValues() != null
? new HashMap<>(changes.getValues())
: new HashMap<>());
existing.setNotes(changes.getNotes());
existing.setTags(changes.getTags() != null
? new ArrayList<>(changes.getTags())
: new ArrayList<>());
existing.setRelatedPageIds(changes.getRelatedPageIds() != null
? new ArrayList<>(changes.getRelatedPageIds())
: new ArrayList<>());
return pageRepository.save(existing);
}
public void deletePage(String id) {
pageRepository.deleteById(id);
}
public boolean pageExists(String id) {
return pageRepository.existsById(id);
}
}

View File

@@ -0,0 +1,84 @@
package com.loremind.application.lorecontext;
import com.loremind.domain.lorecontext.Template;
import com.loremind.domain.lorecontext.ports.TemplateRepository;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Service d'application pour le contexte Template.
* Orchestre la logique métier via le Port TemplateRepository.
* Couche Application de l'Architecture Hexagonale.
*/
@Service
public class TemplateService {
private final TemplateRepository templateRepository;
public TemplateService(TemplateRepository templateRepository) {
this.templateRepository = templateRepository;
}
public Template createTemplate(String loreId,
String name,
String description,
String defaultNodeId,
List<String> fields) {
Template template = Template.builder()
.loreId(loreId)
.name(name)
.description(description)
.defaultNodeId(defaultNodeId)
.fields(fields != null ? new ArrayList<>(fields) : new ArrayList<>())
.build();
return templateRepository.save(template);
}
public Optional<Template> getTemplateById(String id) {
return templateRepository.findById(id);
}
public List<Template> getAllTemplates() {
return templateRepository.findAll();
}
public List<Template> getTemplatesByLoreId(String loreId) {
return templateRepository.findByLoreId(loreId);
}
public List<Template> searchTemplates(String query) {
if (query == null || query.isBlank()) return List.of();
return templateRepository.searchByName(query.trim());
}
/**
* Met à jour un Template existant.
* Pattern Parameter Object via l'entité Template elle-même :
* le controller construit un Template avec les champs à modifier, le service
* recharge l'existant et applique les changements.
*/
public Template updateTemplate(String id, Template changes) {
Template existing = templateRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Template non trouvé avec l'ID: " + id));
existing.setName(changes.getName());
existing.setDescription(changes.getDescription());
existing.setDefaultNodeId(changes.getDefaultNodeId());
existing.setFields(changes.getFields() != null
? new ArrayList<>(changes.getFields())
: new ArrayList<>());
// loreId volontairement immuable : un template ne migre pas d'un Lore à l'autre.
return templateRepository.save(existing);
}
public void deleteTemplate(String id) {
templateRepository.deleteById(id);
}
public boolean templateExists(String id) {
return templateRepository.existsById(id);
}
}

View File

@@ -0,0 +1,60 @@
package com.loremind.domain.campaigncontext;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Entité de domaine représentant un Arc narratif.
* Division majeure d'une Campaign (ex: "L'arc sombre").
* Entité pure du domaine, sans dépendance technique.
*/
@Data
@Builder
public class Arc {
private String id;
private String name;
private String description; // = Synopsis dans l'UI
private String campaignId; // Référence vers la Campaign parente
private int order; // Ordre de l'arc dans la campagne
// Champs narratifs enrichis (voir docs/maquettes/campagne/détail/)
private String themes; // Thèmes principaux explorés dans cet arc
private String stakes; // Enjeux globaux pour les personnages
private String gmNotes; // Notes privées du MJ (non exportées vers FoundryVTT)
private String rewards; // Récompenses et progression
private String resolution; // Dénouement prévu
/**
* IDs des pages du Lore associées à cet arc (weak cross-context references).
* Permet au MJ de lier des PNJ, lieux ou lore items qui jouent un rôle dans cet arc.
* Ne contient que des IDs ; pas d'import du Lore Context (respect des Bounded Contexts).
* Initialisé en {@link ArrayList} vide dans le builder pour éviter les NPE.
*/
@Builder.Default
private List<String> relatedPageIds = new ArrayList<>();
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// ─────────────── Méthodes métier ───────────────
/** Ajoute un lien vers une page du Lore (idempotent : pas de doublon). */
public void linkPage(String pageId) {
if (relatedPageIds == null) relatedPageIds = new ArrayList<>();
if (!relatedPageIds.contains(pageId)) {
relatedPageIds.add(pageId);
this.updatedAt = LocalDateTime.now();
}
}
/** Retire un lien vers une page du Lore (sans erreur si absent). */
public void unlinkPage(String pageId) {
if (relatedPageIds != null && relatedPageIds.remove(pageId)) {
this.updatedAt = LocalDateTime.now();
}
}
}

View File

@@ -0,0 +1,59 @@
package com.loremind.domain.campaigncontext;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Entité de domaine représentant une Campaign.
* Conteneur global pour organiser la narration d'une campagne.
* Entité pure du domaine, sans dépendance technique.
*/
@Data
@Builder
public class Campaign {
private String id;
private String name;
private String description;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private int arcsCount;
/**
* Référence faible (weak reference) vers un Lore.
* Nullable : une campagne peut exister sans univers associé (one-shot, test, pitch libre).
* Ce n'est qu'un ID : le Campaign Context ne dépend PAS du Lore Context
* (respect des Bounded Contexts en DDD).
*/
private String loreId;
/** Associe cette campagne à un Lore existant. */
public void linkToLore(String loreId) {
this.loreId = loreId;
this.updatedAt = LocalDateTime.now();
}
/** Retire l'association à un Lore (la campagne redevient "universe-agnostic"). */
public void unlinkFromLore() {
this.loreId = null;
this.updatedAt = LocalDateTime.now();
}
public boolean isLinkedToLore() {
return this.loreId != null && !this.loreId.isBlank();
}
// Méthode métier pour gérer le nombre d'arcs
public void incrementArcsCount() {
this.arcsCount++;
this.updatedAt = LocalDateTime.now();
}
public void decrementArcsCount() {
if (this.arcsCount > 0) {
this.arcsCount--;
this.updatedAt = LocalDateTime.now();
}
}
}

View File

@@ -0,0 +1,54 @@
package com.loremind.domain.campaigncontext;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Entité de domaine représentant un Chapter.
* Subdivision d'un Arc (ex: "Chapitre 1: Le début").
* Entité pure du domaine, sans dépendance technique.
*/
@Data
@Builder
public class Chapter {
private String id;
private String name;
private String description; // = Synopsis du chapitre dans l'UI
private String arcId; // Référence vers l'Arc parent
private int order; // Ordre du chapitre dans l'arc
// Champs narratifs enrichis (voir docs/maquettes/campagne/détail/)
private String gmNotes; // Notes privées du MJ (non exportées vers FoundryVTT)
private String playerObjectives; // Objectifs des joueurs dans ce chapitre
private String narrativeStakes; // Enjeux narratifs dramatiques
/**
* IDs des pages du Lore associées à ce chapitre (weak cross-context references).
* Permet au MJ de lier des PNJ / lieux / éléments du Lore qui apparaissent dans ce chapitre.
*/
@Builder.Default
private List<String> relatedPageIds = new ArrayList<>();
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// ─────────────── Méthodes métier ───────────────
public void linkPage(String pageId) {
if (relatedPageIds == null) relatedPageIds = new ArrayList<>();
if (!relatedPageIds.contains(pageId)) {
relatedPageIds.add(pageId);
this.updatedAt = LocalDateTime.now();
}
}
public void unlinkPage(String pageId) {
if (relatedPageIds != null && relatedPageIds.remove(pageId)) {
this.updatedAt = LocalDateTime.now();
}
}
}

View File

@@ -0,0 +1,67 @@
package com.loremind.domain.campaigncontext;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Entité de domaine représentant une Scene.
* Unité de jeu la plus fine, subdivision d'un Chapter (ex: "Scène 1: L'auberge").
* Entité pure du domaine, sans dépendance technique.
*/
@Data
@Builder
public class Scene {
private String id;
private String name;
private String description; // = Description courte dans l'UI
private String chapterId; // Référence vers le Chapter parent
private int order; // Ordre de la scène dans le chapitre
// === Contexte et ambiance ===
private String location; // Lieu de la scène (ex: Taverne du Dragon d'Or)
private String timing; // Moment (ex: Soir, à la tombée de la nuit)
private String atmosphere; // Ambiance générale (sons, odeurs, émotions...)
// === Narration pour les joueurs ===
private String playerNarration; // Texte lu directement aux joueurs
// === Notes et secrets du MJ (privé) ===
private String gmSecretNotes; // Informations cachées, non visibles par les joueurs
// === Choix et conséquences ===
private String choicesConsequences; // Options offertes aux joueurs et leurs conséquences
// === Combat ou rencontre ===
private String combatDifficulty; // Difficulté estimée
private String enemies; // Liste des ennemis et créatures
/**
* IDs des pages du Lore associées à cette scène (weak cross-context references).
* Très utile pour la préparation : épingler un lieu, un PNJ, une créature à une scène.
*/
@Builder.Default
private List<String> relatedPageIds = new ArrayList<>();
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// ─────────────── Méthodes métier ───────────────
public void linkPage(String pageId) {
if (relatedPageIds == null) relatedPageIds = new ArrayList<>();
if (!relatedPageIds.contains(pageId)) {
relatedPageIds.add(pageId);
this.updatedAt = LocalDateTime.now();
}
}
public void unlinkPage(String pageId) {
if (relatedPageIds != null && relatedPageIds.remove(pageId)) {
this.updatedAt = LocalDateTime.now();
}
}
}

View File

@@ -0,0 +1,24 @@
package com.loremind.domain.campaigncontext.ports;
import com.loremind.domain.campaigncontext.Arc;
import java.util.List;
import java.util.Optional;
/**
* Port de sortie pour la persistance des Arcs.
* Interface définie dans le domaine, implémentée par l'infrastructure.
*/
public interface ArcRepository {
Arc save(Arc arc);
Optional<Arc> findById(String id);
List<Arc> findByCampaignId(String campaignId);
List<Arc> findAll();
void deleteById(String id);
boolean existsById(String id);
}

View File

@@ -0,0 +1,24 @@
package com.loremind.domain.campaigncontext.ports;
import com.loremind.domain.campaigncontext.Campaign;
import java.util.List;
import java.util.Optional;
/**
* Port de sortie pour la persistance des Campaigns.
* Interface définie dans le domaine, implémentée par l'infrastructure.
*/
public interface CampaignRepository {
Campaign save(Campaign campaign);
Optional<Campaign> findById(String id);
List<Campaign> findAll();
void deleteById(String id);
boolean existsById(String id);
List<Campaign> searchByName(String query);
}

View File

@@ -0,0 +1,24 @@
package com.loremind.domain.campaigncontext.ports;
import com.loremind.domain.campaigncontext.Chapter;
import java.util.List;
import java.util.Optional;
/**
* Port de sortie pour la persistance des Chapters.
* Interface définie dans le domaine, implémentée par l'infrastructure.
*/
public interface ChapterRepository {
Chapter save(Chapter chapter);
Optional<Chapter> findById(String id);
List<Chapter> findByArcId(String arcId);
List<Chapter> findAll();
void deleteById(String id);
boolean existsById(String id);
}

View File

@@ -0,0 +1,24 @@
package com.loremind.domain.campaigncontext.ports;
import com.loremind.domain.campaigncontext.Scene;
import java.util.List;
import java.util.Optional;
/**
* Port de sortie pour la persistance des Scenes.
* Interface définie dans le domaine, implémentée par l'infrastructure.
*/
public interface SceneRepository {
Scene save(Scene scene);
Optional<Scene> findById(String id);
List<Scene> findByChapterId(String chapterId);
List<Scene> findAll();
void deleteById(String id);
boolean existsById(String id);
}

View File

@@ -0,0 +1,48 @@
package com.loremind.domain.lorecontext;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Entité de domaine représentant un Univers de jeu (Lore).
* Conteneur global pour organiser la connaissance d'un monde.
* C'est une entité pure du domaine, sans dépendance technique (pas de JPA).
*/
@Data
@Builder
public class Lore {
private String id;
private String name;
private String description;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private int nodeCount;
private int pageCount;
// Méthodes métier pour gérer les métriques
public void incrementNodeCount() {
this.nodeCount++;
this.updatedAt = LocalDateTime.now();
}
public void decrementNodeCount() {
if (this.nodeCount > 0) {
this.nodeCount--;
this.updatedAt = LocalDateTime.now();
}
}
public void incrementPageCount() {
this.pageCount++;
this.updatedAt = LocalDateTime.now();
}
public void decrementPageCount() {
if (this.pageCount > 0) {
this.pageCount--;
this.updatedAt = LocalDateTime.now();
}
}
}

View File

@@ -0,0 +1,42 @@
package com.loremind.domain.lorecontext;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Entité de domaine représentant un dossier dans l'arborescence d'un Lore.
* Un dossier organise des Pages et peut contenir des sous-dossiers.
* Structure hiérarchique via parentId (auto-référence).
* Entité pure du domaine, sans dépendance technique.
*
* Note : le nom interne reste "LoreNode" pour des raisons historiques et
* techniques (compatibilité BDD, couplage avec le code existant). L'UI expose
* le concept sous le terme "dossier".
*/
@Data
@Builder
public class LoreNode {
private String id;
private String name;
/** Clé de l'icône lucide choisie par l'utilisateur (ex: "users", "map-pin"). */
private String icon;
private String parentId; // Auto-référence pour l'arborescence
private String loreId; // Référence vers le Lore parent
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Méthode métier pour vérifier si c'est un nœud racine
public boolean isRoot() {
return parentId == null || parentId.isEmpty();
}
// Méthode métier pour vérifier si c'est un nœud feuille (sans enfants)
// Note: La vérification réelle nécessite d'accéder aux enfants,
// donc cette méthode est indicative et doit être complétée par le repository
public boolean isLeaf() {
// Cette logique sera implémentée au niveau du service/repository
return false;
}
}

View File

@@ -0,0 +1,82 @@
package com.loremind.domain.lorecontext;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Entité de domaine représentant une page de contenu rattachée à un LoreNode.
*
* Une page :
* - appartient à un Lore (loreId — dénormalisé pour queries rapides)
* - est rangée sous un LoreNode (nodeId)
* - est générée depuis un Template (templateId) dont elle suit les `fields`
* - stocke les valeurs de ses champs dynamiques dans `values` (fieldName → value)
* - porte des métadonnées éditoriales : notes privées MJ, tags, liens inter-pages.
*
* Entité pure du domaine : aucune dépendance technique.
*/
@Data
@Builder
public class Page {
private String id;
private String loreId;
private String nodeId;
private String templateId;
private String title;
/** Valeurs des champs dynamiques définis par le Template. */
private Map<String, String> values;
/** Notes privées du MJ (non exportées vers FoundryVTT). */
private String notes;
/** Étiquettes libres pour regroupement/recherche. */
private List<String> tags;
/** IDs d'autres Pages liées (mêmes Lore ou cross-lore). */
private List<String> relatedPageIds;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// --- Méthodes métier --------------------------------------------------
/** Met à jour la valeur d'un champ dynamique (création de la map si absente). */
public void setFieldValue(String fieldName, String value) {
if (values == null) {
values = new HashMap<>();
}
values.put(fieldName, value);
this.updatedAt = LocalDateTime.now();
}
public String getFieldValue(String fieldName) {
return values == null ? null : values.get(fieldName);
}
public void addTag(String tag) {
if (tag == null || tag.isBlank()) return;
if (tags == null) tags = new ArrayList<>();
if (!tags.contains(tag)) {
tags.add(tag);
this.updatedAt = LocalDateTime.now();
}
}
public void removeTag(String tag) {
if (tags != null && tags.remove(tag)) {
this.updatedAt = LocalDateTime.now();
}
}
public boolean hasTemplate() {
return templateId != null && !templateId.isBlank();
}
}

View File

@@ -0,0 +1,61 @@
package com.loremind.domain.lorecontext;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Entité de domaine représentant un Template pour la génération de Pages.
*
* Un Template :
* - appartient à un Lore (loreId)
* - définit le noeud par défaut où seront rangées les Pages créées (defaultNodeId)
* - porte une liste ordonnée de noms de champs dynamiques (fields)
* qui seront instanciés sur chaque Page produite depuis ce gabarit.
*
* Entité pure du domaine : aucune dépendance technique (Spring, JPA, etc.).
*/
@Data
@Builder
public class Template {
private String id;
private String loreId; // Rattachement au Lore propriétaire
private String name;
private String description;
private String defaultNodeId; // Noeud cible des Pages générées
private List<String> fields; // Noms des champs dynamiques (ordonnés)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// --- Méthodes métier ---------------------------------------------------
/** Nombre de champs dynamiques définis (affiché dans le sidebar "X champs"). */
public int fieldCount() {
return fields == null ? 0 : fields.size();
}
/** Ajoute un champ à la fin de la liste (ignore les doublons et les blancs). */
public void addField(String fieldName) {
if (fieldName == null || fieldName.isBlank()) {
return;
}
if (fields == null) {
fields = new ArrayList<>();
}
if (!fields.contains(fieldName)) {
fields.add(fieldName);
this.updatedAt = LocalDateTime.now();
}
}
/** Retire un champ s'il existe. */
public void removeField(String fieldName) {
if (fields != null && fields.remove(fieldName)) {
this.updatedAt = LocalDateTime.now();
}
}
}

View File

@@ -0,0 +1,30 @@
package com.loremind.domain.lorecontext.ports;
import com.loremind.domain.lorecontext.LoreNode;
import java.util.List;
import java.util.Optional;
/**
* Port de sortie pour la persistance des LoreNodes.
* Interface définie dans le domaine, implémentée par l'infrastructure.
*/
public interface LoreNodeRepository {
LoreNode save(LoreNode loreNode);
Optional<LoreNode> findById(String id);
List<LoreNode> findByLoreId(String loreId);
List<LoreNode> findByParentId(String parentId);
List<LoreNode> findAll();
void deleteById(String id);
boolean existsById(String id);
long countByLoreId(String loreId);
List<LoreNode> searchByName(String query);
}

View File

@@ -0,0 +1,25 @@
package com.loremind.domain.lorecontext.ports;
import com.loremind.domain.lorecontext.Lore;
import java.util.List;
import java.util.Optional;
/**
* Port de sortie pour la persistance des Lores.
* Interface définie dans le domaine, implémentée par l'infrastructure.
* C'est un Port de sortie (Output Port) selon l'Architecture Hexagonale.
*/
public interface LoreRepository {
Lore save(Lore lore);
Optional<Lore> findById(String id);
List<Lore> findAll();
void deleteById(String id);
boolean existsById(String id);
List<Lore> searchByName(String query);
}

View File

@@ -0,0 +1,32 @@
package com.loremind.domain.lorecontext.ports;
import com.loremind.domain.lorecontext.Page;
import java.util.List;
import java.util.Optional;
/**
* Port de sortie pour la persistance des Pages.
* Interface définie dans le domaine, implémentée par l'infrastructure.
*/
public interface PageRepository {
Page save(Page page);
Optional<Page> findById(String id);
List<Page> findByLoreId(String loreId);
List<Page> findByNodeId(String nodeId);
List<Page> findByTemplateId(String templateId);
List<Page> findAll();
void deleteById(String id);
boolean existsById(String id);
long countByLoreId(String loreId);
List<Page> searchByTitle(String query);
}

View File

@@ -0,0 +1,27 @@
package com.loremind.domain.lorecontext.ports;
import com.loremind.domain.lorecontext.Template;
import java.util.List;
import java.util.Optional;
/**
* Port de sortie pour la persistance des Templates.
* Interface définie dans le domaine, implémentée par l'infrastructure.
*/
public interface TemplateRepository {
Template save(Template template);
Optional<Template> findById(String id);
List<Template> findAll();
/** Tous les templates rattachés à un Lore donné (pour le panneau sidebar). */
List<Template> findByLoreId(String loreId);
void deleteById(String id);
boolean existsById(String id);
List<Template> searchByName(String query);
}

View File

@@ -0,0 +1,44 @@
package com.loremind.infrastructure.persistence.converter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.Map;
/**
* Converter JPA pour convertir Map<String, Object> en String (JSON).
* Compatible avec PostgreSQL (JSONB peut stocker du JSON dans TEXT).
* Utilisé pour le champ structure de Template, mais peut servir pour tout champ JSON.
* C'est un converter générique réutilisable.
*/
@Converter(autoApply = false)
public class MapJsonConverter implements AttributeConverter<Map<String, Object>, String> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(Map<String, Object> attribute) {
if (attribute == null) {
return null;
}
try {
return objectMapper.writeValueAsString(attribute);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Erreur lors de la conversion Map vers JSON String", e);
}
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
}
try {
return objectMapper.readValue(dbData, Map.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Erreur lors de la conversion JSON String vers Map", e);
}
}
}

View File

@@ -0,0 +1,46 @@
package com.loremind.infrastructure.persistence.converter;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.Collections;
import java.util.List;
/**
* Convertit une List<String> du domaine en chaîne JSON stockée en base, et
* inversement. Utilisé pour les listes simples (ex: Template.fields).
*
* Ceci est un adaptateur technique d'infrastructure : il permet au domaine de
* rester pur (juste une List<String>) pendant que JPA parle JSON à PostgreSQL.
*/
@Converter
public class StringListJsonConverter implements AttributeConverter<List<String>, String> {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public String convertToDatabaseColumn(List<String> attribute) {
if (attribute == null || attribute.isEmpty()) {
return "[]";
}
try {
return MAPPER.writeValueAsString(attribute);
} catch (Exception e) {
throw new IllegalStateException("Erreur sérialisation List<String> → JSON", e);
}
}
@Override
public List<String> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.isBlank()) {
return Collections.emptyList();
}
try {
return MAPPER.readValue(dbData, new TypeReference<List<String>>() {});
} catch (Exception e) {
throw new IllegalStateException("Erreur désérialisation JSON → List<String>", e);
}
}
}

View File

@@ -0,0 +1,44 @@
package com.loremind.infrastructure.persistence.converter;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.Collections;
import java.util.Map;
/**
* Convertit une Map<String,String> du domaine en chaîne JSON et inversement.
* Utilisé pour les maps clé-valeur simples (ex: Page.values — stocke les valeurs
* des champs dynamiques définis par le Template).
*/
@Converter
public class StringMapJsonConverter implements AttributeConverter<Map<String, String>, String> {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public String convertToDatabaseColumn(Map<String, String> attribute) {
if (attribute == null || attribute.isEmpty()) {
return "{}";
}
try {
return MAPPER.writeValueAsString(attribute);
} catch (Exception e) {
throw new IllegalStateException("Erreur sérialisation Map<String,String> → JSON", e);
}
}
@Override
public Map<String, String> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.isBlank()) {
return Collections.emptyMap();
}
try {
return MAPPER.readValue(dbData, new TypeReference<Map<String, String>>() {});
} catch (Exception e) {
throw new IllegalStateException("Erreur désérialisation JSON → Map<String,String>", e);
}
}
}

View File

@@ -0,0 +1,81 @@
package com.loremind.infrastructure.persistence.entity;
import com.loremind.infrastructure.persistence.converter.StringListJsonConverter;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Entité JPA pour la persistance des Arcs en base de données PostgreSQL.
*/
@Entity
@Table(name = "arcs")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ArcJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "campaign_id", nullable = false)
private Long campaignId;
@Column(name = "\"order\"", nullable = false)
private int order;
// Champs narratifs enrichis — ajoutés automatiquement par Hibernate DDL (ddl-auto=update)
@Column(columnDefinition = "TEXT")
private String themes;
@Column(columnDefinition = "TEXT")
private String stakes;
@Column(name = "gm_notes", columnDefinition = "TEXT")
private String gmNotes;
@Column(columnDefinition = "TEXT")
private String rewards;
@Column(columnDefinition = "TEXT")
private String resolution;
/**
* IDs des pages du Lore liées à cet arc, stockés en JSON dans une colonne TEXT.
* Pas de FK cross-context : respect des Bounded Contexts.
*/
@Column(name = "related_page_ids", columnDefinition = "TEXT")
@Convert(converter = StringListJsonConverter.class)
@Builder.Default
private List<String> relatedPageIds = new ArrayList<>();
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,58 @@
package com.loremind.infrastructure.persistence.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Entité JPA pour la persistance des Campaigns en base de données PostgreSQL.
*/
@Entity
@Table(name = "campaigns")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CampaignJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@Column(name = "arcs_count", nullable = false)
private int arcsCount;
/**
* ID du Lore associé (nullable).
* Pas de @ManyToOne / pas de FK : c'est une weak reference inter-contexte.
* Le Campaign Context et le Lore Context doivent pouvoir évoluer indépendamment.
*/
@Column(name = "lore_id")
private String loreId;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,71 @@
package com.loremind.infrastructure.persistence.entity;
import com.loremind.infrastructure.persistence.converter.StringListJsonConverter;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Entité JPA pour la persistance des Chapters en base de données PostgreSQL.
*/
@Entity
@Table(name = "chapters")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChapterJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "arc_id", nullable = false)
private Long arcId;
@Column(name = "\"order\"", nullable = false)
private int order;
// Champs narratifs enrichis — ajoutés automatiquement par Hibernate DDL (ddl-auto=update)
@Column(name = "gm_notes", columnDefinition = "TEXT")
private String gmNotes;
@Column(name = "player_objectives", columnDefinition = "TEXT")
private String playerObjectives;
@Column(name = "narrative_stakes", columnDefinition = "TEXT")
private String narrativeStakes;
@Column(name = "related_page_ids", columnDefinition = "TEXT")
@Convert(converter = StringListJsonConverter.class)
@Builder.Default
private List<String> relatedPageIds = new ArrayList<>();
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,54 @@
package com.loremind.infrastructure.persistence.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Entité JPA pour la persistance des Lores en base de données PostgreSQL.
* Cette classe contient des annotations JPA, donc elle est dans infrastructure, pas dans le domaine.
*/
@Entity
@Table(name = "lores")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoreJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@Column(name = "node_count", nullable = false)
private int nodeCount;
@Column(name = "page_count", nullable = false)
private int pageCount;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,55 @@
package com.loremind.infrastructure.persistence.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Entité JPA pour la persistance des LoreNodes en base de données PostgreSQL.
* Structure hiérarchique via parentId (auto-référence).
*/
@Entity
@Table(name = "lore_nodes")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoreNodeJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
/** Clé de l'icône lucide-angular (ex: "users", "map-pin"). Nullable : un dossier peut ne pas avoir d'icône. */
@Column(length = 64)
private String icon;
@Column(name = "parent_id")
private Long parentId; // Auto-référence pour l'arborescence
@Column(name = "lore_id", nullable = false)
private Long loreId;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,76 @@
package com.loremind.infrastructure.persistence.entity;
import com.loremind.infrastructure.persistence.converter.StringListJsonConverter;
import com.loremind.infrastructure.persistence.converter.StringMapJsonConverter;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* Entité JPA pour la persistance des Pages en PostgreSQL.
* - lore_id dénormalisé (accès rapide aux pages d'un lore sans passer par nodes).
* - values / tags / related_page_ids stockés en JSON (TEXT via converters).
* - Note : colonne `values_json` car `values` est un mot-clé SQL réservé.
*/
@Entity
@Table(name = "pages")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "lore_id", nullable = false)
private Long loreId;
@Column(name = "node_id", nullable = false)
private Long nodeId;
@Column(name = "template_id")
private Long templateId;
@Column(nullable = false)
private String title;
@Column(name = "values_json", columnDefinition = "TEXT")
@Convert(converter = StringMapJsonConverter.class)
private Map<String, String> values;
@Column(columnDefinition = "TEXT")
private String notes;
@Column(name = "tags", columnDefinition = "TEXT")
@Convert(converter = StringListJsonConverter.class)
private List<String> tags;
@Column(name = "related_page_ids", columnDefinition = "TEXT")
@Convert(converter = StringListJsonConverter.class)
private List<String> relatedPageIds;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,92 @@
package com.loremind.infrastructure.persistence.entity;
import com.loremind.infrastructure.persistence.converter.StringListJsonConverter;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Entité JPA pour la persistance des Scenes en base de données PostgreSQL.
*/
@Entity
@Table(name = "scenes")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SceneJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "chapter_id", nullable = false)
private Long chapterId;
@Column(name = "\"order\"", nullable = false)
private int order;
// Champs narratifs enrichis — ajoutés automatiquement par Hibernate (ddl-auto=update)
// Contexte et ambiance
@Column(columnDefinition = "TEXT")
private String location;
@Column(columnDefinition = "TEXT")
private String timing;
@Column(columnDefinition = "TEXT")
private String atmosphere;
// Narration
@Column(name = "player_narration", columnDefinition = "TEXT")
private String playerNarration;
// Secrets MJ
@Column(name = "gm_secret_notes", columnDefinition = "TEXT")
private String gmSecretNotes;
// Choix et conséquences
@Column(name = "choices_consequences", columnDefinition = "TEXT")
private String choicesConsequences;
// Combat
@Column(name = "combat_difficulty", columnDefinition = "TEXT")
private String combatDifficulty;
@Column(columnDefinition = "TEXT")
private String enemies;
@Column(name = "related_page_ids", columnDefinition = "TEXT")
@Convert(converter = StringListJsonConverter.class)
@Builder.Default
private List<String> relatedPageIds = new ArrayList<>();
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,63 @@
package com.loremind.infrastructure.persistence.entity;
import com.loremind.infrastructure.persistence.converter.StringListJsonConverter;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* Entité JPA pour la persistance des Templates en PostgreSQL.
* - loreId et defaultNodeId : colonnes typées (FK logiques, pas de @ManyToOne
* pour respecter l'isolation des Bounded Contexts).
* - fields : stocké en JSON (TEXT) via StringListJsonConverter.
*/
@Entity
@Table(name = "templates")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TemplateJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "lore_id", nullable = false)
private Long loreId;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "default_node_id")
private Long defaultNodeId;
@Column(name = "fields", columnDefinition = "TEXT")
@Convert(converter = StringListJsonConverter.class)
private List<String> fields;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,16 @@
package com.loremind.infrastructure.persistence.jpa;
import com.loremind.infrastructure.persistence.entity.ArcJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository Spring Data JPA pour ArcJpaEntity.
*/
@Repository
public interface ArcJpaRepository extends JpaRepository<ArcJpaEntity, Long> {
List<ArcJpaEntity> findByCampaignId(Long campaignId);
}

View File

@@ -0,0 +1,19 @@
package com.loremind.infrastructure.persistence.jpa;
import com.loremind.infrastructure.persistence.entity.CampaignJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository Spring Data JPA pour CampaignJpaEntity.
*/
@Repository
public interface CampaignJpaRepository extends JpaRepository<CampaignJpaEntity, Long> {
@Query("SELECT c FROM CampaignJpaEntity c WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :query, '%'))")
List<CampaignJpaEntity> findByNameContainingIgnoreCase(@Param("query") String query);
}

View File

@@ -0,0 +1,16 @@
package com.loremind.infrastructure.persistence.jpa;
import com.loremind.infrastructure.persistence.entity.ChapterJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository Spring Data JPA pour ChapterJpaEntity.
*/
@Repository
public interface ChapterJpaRepository extends JpaRepository<ChapterJpaEntity, Long> {
List<ChapterJpaEntity> findByArcId(Long arcId);
}

View File

@@ -0,0 +1,17 @@
package com.loremind.infrastructure.persistence.jpa;
import com.loremind.infrastructure.persistence.entity.LoreJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository Spring Data JPA pour LoreJpaEntity.
* Utilisé par l'adaptateur PostgresLoreRepository.
*/
@Repository
public interface LoreJpaRepository extends JpaRepository<LoreJpaEntity, Long> {
List<LoreJpaEntity> findByNameContainingIgnoreCase(String query);
}

View File

@@ -0,0 +1,22 @@
package com.loremind.infrastructure.persistence.jpa;
import com.loremind.infrastructure.persistence.entity.LoreNodeJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository Spring Data JPA pour LoreNodeJpaEntity.
*/
@Repository
public interface LoreNodeJpaRepository extends JpaRepository<LoreNodeJpaEntity, Long> {
List<LoreNodeJpaEntity> findByLoreId(Long loreId);
List<LoreNodeJpaEntity> findByParentId(Long parentId);
long countByLoreId(Long loreId);
List<LoreNodeJpaEntity> findByNameContainingIgnoreCase(String query);
}

View File

@@ -0,0 +1,24 @@
package com.loremind.infrastructure.persistence.jpa;
import com.loremind.infrastructure.persistence.entity.PageJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository Spring Data JPA pour PageJpaEntity.
*/
@Repository
public interface PageJpaRepository extends JpaRepository<PageJpaEntity, Long> {
List<PageJpaEntity> findByLoreId(Long loreId);
List<PageJpaEntity> findByNodeId(Long nodeId);
List<PageJpaEntity> findByTemplateId(Long templateId);
long countByLoreId(Long loreId);
List<PageJpaEntity> findByTitleContainingIgnoreCase(String query);
}

View File

@@ -0,0 +1,16 @@
package com.loremind.infrastructure.persistence.jpa;
import com.loremind.infrastructure.persistence.entity.SceneJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository Spring Data JPA pour SceneJpaEntity.
*/
@Repository
public interface SceneJpaRepository extends JpaRepository<SceneJpaEntity, Long> {
List<SceneJpaEntity> findByChapterId(Long chapterId);
}

View File

@@ -0,0 +1,18 @@
package com.loremind.infrastructure.persistence.jpa;
import com.loremind.infrastructure.persistence.entity.TemplateJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository Spring Data JPA pour TemplateJpaEntity.
*/
@Repository
public interface TemplateJpaRepository extends JpaRepository<TemplateJpaEntity, Long> {
List<TemplateJpaEntity> findByLoreId(Long loreId);
List<TemplateJpaEntity> findByNameContainingIgnoreCase(String query);
}

View File

@@ -0,0 +1,108 @@
package com.loremind.infrastructure.persistence.postgres;
import com.loremind.domain.campaigncontext.Arc;
import com.loremind.domain.campaigncontext.ports.ArcRepository;
import com.loremind.infrastructure.persistence.entity.ArcJpaEntity;
import com.loremind.infrastructure.persistence.jpa.ArcJpaRepository;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Adaptateur d'infrastructure qui implémente le Port ArcRepository.
*/
@Repository
public class PostgresArcRepository implements ArcRepository {
private final ArcJpaRepository jpaRepository;
public PostgresArcRepository(ArcJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public Arc save(Arc arc) {
ArcJpaEntity jpaEntity = toJpaEntity(arc);
ArcJpaEntity saved = jpaRepository.save(jpaEntity);
return toDomainEntity(saved);
}
@Override
public Optional<Arc> findById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.findById(longId)
.map(this::toDomainEntity);
}
@Override
public List<Arc> findByCampaignId(String campaignId) {
Long longCampaignId = Long.parseLong(campaignId);
return jpaRepository.findByCampaignId(longCampaignId).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public List<Arc> findAll() {
return jpaRepository.findAll().stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public void deleteById(String id) {
Long longId = Long.parseLong(id);
jpaRepository.deleteById(longId);
}
@Override
public boolean existsById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.existsById(longId);
}
private Arc toDomainEntity(ArcJpaEntity jpaEntity) {
return Arc.builder()
.id(jpaEntity.getId().toString())
.name(jpaEntity.getName())
.description(jpaEntity.getDescription())
.campaignId(jpaEntity.getCampaignId().toString())
.order(jpaEntity.getOrder())
.themes(jpaEntity.getThemes())
.stakes(jpaEntity.getStakes())
.gmNotes(jpaEntity.getGmNotes())
.rewards(jpaEntity.getRewards())
.resolution(jpaEntity.getResolution())
// Defensive copy : jamais de null en domaine pour simplifier les appelants.
.relatedPageIds(jpaEntity.getRelatedPageIds() != null
? new ArrayList<>(jpaEntity.getRelatedPageIds())
: new ArrayList<>())
.createdAt(jpaEntity.getCreatedAt())
.updatedAt(jpaEntity.getUpdatedAt())
.build();
}
private ArcJpaEntity toJpaEntity(Arc arc) {
Long id = arc.getId() != null ? Long.parseLong(arc.getId()) : null;
return ArcJpaEntity.builder()
.id(id)
.name(arc.getName())
.description(arc.getDescription())
.campaignId(Long.parseLong(arc.getCampaignId()))
.order(arc.getOrder())
.themes(arc.getThemes())
.stakes(arc.getStakes())
.gmNotes(arc.getGmNotes())
.rewards(arc.getRewards())
.resolution(arc.getResolution())
.relatedPageIds(arc.getRelatedPageIds() != null
? new ArrayList<>(arc.getRelatedPageIds())
: new ArrayList<>())
.createdAt(arc.getCreatedAt())
.updatedAt(arc.getUpdatedAt())
.build();
}
}

View File

@@ -0,0 +1,89 @@
package com.loremind.infrastructure.persistence.postgres;
import com.loremind.domain.campaigncontext.Campaign;
import com.loremind.domain.campaigncontext.ports.CampaignRepository;
import com.loremind.infrastructure.persistence.entity.CampaignJpaEntity;
import com.loremind.infrastructure.persistence.jpa.CampaignJpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Adaptateur d'infrastructure qui implémente le Port CampaignRepository.
*/
@Repository
public class PostgresCampaignRepository implements CampaignRepository {
private final CampaignJpaRepository jpaRepository;
public PostgresCampaignRepository(CampaignJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public Campaign save(Campaign campaign) {
CampaignJpaEntity jpaEntity = toJpaEntity(campaign);
CampaignJpaEntity saved = jpaRepository.save(jpaEntity);
return toDomainEntity(saved);
}
@Override
public Optional<Campaign> findById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.findById(longId)
.map(this::toDomainEntity);
}
@Override
public List<Campaign> findAll() {
return jpaRepository.findAll().stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public void deleteById(String id) {
Long longId = Long.parseLong(id);
jpaRepository.deleteById(longId);
}
@Override
public boolean existsById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.existsById(longId);
}
@Override
public List<Campaign> searchByName(String query) {
return jpaRepository.findByNameContainingIgnoreCase(query).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
private Campaign toDomainEntity(CampaignJpaEntity jpaEntity) {
return Campaign.builder()
.id(jpaEntity.getId().toString())
.name(jpaEntity.getName())
.description(jpaEntity.getDescription())
.createdAt(jpaEntity.getCreatedAt())
.updatedAt(jpaEntity.getUpdatedAt())
.arcsCount(jpaEntity.getArcsCount())
.loreId(jpaEntity.getLoreId())
.build();
}
private CampaignJpaEntity toJpaEntity(Campaign campaign) {
Long id = campaign.getId() != null ? Long.parseLong(campaign.getId()) : null;
return CampaignJpaEntity.builder()
.id(id)
.name(campaign.getName())
.description(campaign.getDescription())
.createdAt(campaign.getCreatedAt())
.updatedAt(campaign.getUpdatedAt())
.arcsCount(campaign.getArcsCount())
.loreId(campaign.getLoreId())
.build();
}
}

View File

@@ -0,0 +1,103 @@
package com.loremind.infrastructure.persistence.postgres;
import com.loremind.domain.campaigncontext.Chapter;
import com.loremind.domain.campaigncontext.ports.ChapterRepository;
import com.loremind.infrastructure.persistence.entity.ChapterJpaEntity;
import com.loremind.infrastructure.persistence.jpa.ChapterJpaRepository;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Adaptateur d'infrastructure qui implémente le Port ChapterRepository.
*/
@Repository
public class PostgresChapterRepository implements ChapterRepository {
private final ChapterJpaRepository jpaRepository;
public PostgresChapterRepository(ChapterJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public Chapter save(Chapter chapter) {
ChapterJpaEntity jpaEntity = toJpaEntity(chapter);
ChapterJpaEntity saved = jpaRepository.save(jpaEntity);
return toDomainEntity(saved);
}
@Override
public Optional<Chapter> findById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.findById(longId)
.map(this::toDomainEntity);
}
@Override
public List<Chapter> findByArcId(String arcId) {
Long longArcId = Long.parseLong(arcId);
return jpaRepository.findByArcId(longArcId).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public List<Chapter> findAll() {
return jpaRepository.findAll().stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public void deleteById(String id) {
Long longId = Long.parseLong(id);
jpaRepository.deleteById(longId);
}
@Override
public boolean existsById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.existsById(longId);
}
private Chapter toDomainEntity(ChapterJpaEntity jpaEntity) {
return Chapter.builder()
.id(jpaEntity.getId().toString())
.name(jpaEntity.getName())
.description(jpaEntity.getDescription())
.arcId(jpaEntity.getArcId().toString())
.order(jpaEntity.getOrder())
.gmNotes(jpaEntity.getGmNotes())
.playerObjectives(jpaEntity.getPlayerObjectives())
.narrativeStakes(jpaEntity.getNarrativeStakes())
.relatedPageIds(jpaEntity.getRelatedPageIds() != null
? new ArrayList<>(jpaEntity.getRelatedPageIds())
: new ArrayList<>())
.createdAt(jpaEntity.getCreatedAt())
.updatedAt(jpaEntity.getUpdatedAt())
.build();
}
private ChapterJpaEntity toJpaEntity(Chapter chapter) {
Long id = chapter.getId() != null ? Long.parseLong(chapter.getId()) : null;
return ChapterJpaEntity.builder()
.id(id)
.name(chapter.getName())
.description(chapter.getDescription())
.arcId(Long.parseLong(chapter.getArcId()))
.order(chapter.getOrder())
.gmNotes(chapter.getGmNotes())
.playerObjectives(chapter.getPlayerObjectives())
.narrativeStakes(chapter.getNarrativeStakes())
.relatedPageIds(chapter.getRelatedPageIds() != null
? new ArrayList<>(chapter.getRelatedPageIds())
: new ArrayList<>())
.createdAt(chapter.getCreatedAt())
.updatedAt(chapter.getUpdatedAt())
.build();
}
}

View File

@@ -0,0 +1,118 @@
package com.loremind.infrastructure.persistence.postgres;
import com.loremind.domain.lorecontext.LoreNode;
import com.loremind.domain.lorecontext.ports.LoreNodeRepository;
import com.loremind.infrastructure.persistence.entity.LoreNodeJpaEntity;
import com.loremind.infrastructure.persistence.jpa.LoreNodeJpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Adaptateur d'infrastructure qui implémente le Port LoreNodeRepository.
*/
@Repository
public class PostgresLoreNodeRepository implements LoreNodeRepository {
private final LoreNodeJpaRepository jpaRepository;
public PostgresLoreNodeRepository(LoreNodeJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public LoreNode save(LoreNode loreNode) {
LoreNodeJpaEntity jpaEntity = toJpaEntity(loreNode);
LoreNodeJpaEntity saved = jpaRepository.save(jpaEntity);
return toDomainEntity(saved);
}
@Override
public Optional<LoreNode> findById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.findById(longId)
.map(this::toDomainEntity);
}
@Override
public List<LoreNode> findByLoreId(String loreId) {
Long longLoreId = Long.parseLong(loreId);
return jpaRepository.findByLoreId(longLoreId).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public List<LoreNode> findByParentId(String parentId) {
if (parentId == null || parentId.isEmpty()) {
return jpaRepository.findByParentId(null).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
Long longParentId = Long.parseLong(parentId);
return jpaRepository.findByParentId(longParentId).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public List<LoreNode> findAll() {
return jpaRepository.findAll().stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public void deleteById(String id) {
Long longId = Long.parseLong(id);
jpaRepository.deleteById(longId);
}
@Override
public boolean existsById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.existsById(longId);
}
@Override
public long countByLoreId(String loreId) {
return jpaRepository.countByLoreId(Long.parseLong(loreId));
}
@Override
public List<LoreNode> searchByName(String query) {
return jpaRepository.findByNameContainingIgnoreCase(query).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
private LoreNode toDomainEntity(LoreNodeJpaEntity jpaEntity) {
return LoreNode.builder()
.id(jpaEntity.getId().toString())
.name(jpaEntity.getName())
.icon(jpaEntity.getIcon())
.parentId(jpaEntity.getParentId() != null ? jpaEntity.getParentId().toString() : null)
.loreId(jpaEntity.getLoreId().toString())
.createdAt(jpaEntity.getCreatedAt())
.updatedAt(jpaEntity.getUpdatedAt())
.build();
}
private LoreNodeJpaEntity toJpaEntity(LoreNode loreNode) {
Long id = loreNode.getId() != null ? Long.parseLong(loreNode.getId()) : null;
Long parentId = loreNode.getParentId() != null && !loreNode.getParentId().isEmpty()
? Long.parseLong(loreNode.getParentId())
: null;
return LoreNodeJpaEntity.builder()
.id(id)
.name(loreNode.getName())
.icon(loreNode.getIcon())
.parentId(parentId)
.loreId(Long.parseLong(loreNode.getLoreId()))
.createdAt(loreNode.getCreatedAt())
.updatedAt(loreNode.getUpdatedAt())
.build();
}
}

View File

@@ -0,0 +1,92 @@
package com.loremind.infrastructure.persistence.postgres;
import com.loremind.domain.lorecontext.Lore;
import com.loremind.domain.lorecontext.ports.LoreRepository;
import com.loremind.infrastructure.persistence.entity.LoreJpaEntity;
import com.loremind.infrastructure.persistence.jpa.LoreJpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Adaptateur d'infrastructure qui implémente le Port LoreRepository.
* Utilise LoreJpaRepository pour interagir avec PostgreSQL.
* Fait la conversion entre Lore (domaine) et LoreJpaEntity (JPA).
*/
@Repository
public class PostgresLoreRepository implements LoreRepository {
private final LoreJpaRepository jpaRepository;
public PostgresLoreRepository(LoreJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public Lore save(Lore lore) {
LoreJpaEntity jpaEntity = toJpaEntity(lore);
LoreJpaEntity saved = jpaRepository.save(jpaEntity);
return toDomainEntity(saved);
}
@Override
public Optional<Lore> findById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.findById(longId)
.map(this::toDomainEntity);
}
@Override
public List<Lore> findAll() {
return jpaRepository.findAll().stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public void deleteById(String id) {
Long longId = Long.parseLong(id);
jpaRepository.deleteById(longId);
}
@Override
public boolean existsById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.existsById(longId);
}
@Override
public List<Lore> searchByName(String query) {
return jpaRepository.findByNameContainingIgnoreCase(query).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
// Méthodes de conversion
private Lore toDomainEntity(LoreJpaEntity jpaEntity) {
return Lore.builder()
.id(jpaEntity.getId().toString())
.name(jpaEntity.getName())
.description(jpaEntity.getDescription())
.createdAt(jpaEntity.getCreatedAt())
.updatedAt(jpaEntity.getUpdatedAt())
.nodeCount(jpaEntity.getNodeCount())
.pageCount(jpaEntity.getPageCount())
.build();
}
private LoreJpaEntity toJpaEntity(Lore lore) {
Long id = lore.getId() != null ? Long.parseLong(lore.getId()) : null;
return LoreJpaEntity.builder()
.id(id)
.name(lore.getName())
.description(lore.getDescription())
.createdAt(lore.getCreatedAt())
.updatedAt(lore.getUpdatedAt())
.nodeCount(lore.getNodeCount())
.pageCount(lore.getPageCount())
.build();
}
}

View File

@@ -0,0 +1,130 @@
package com.loremind.infrastructure.persistence.postgres;
import com.loremind.domain.lorecontext.Page;
import com.loremind.domain.lorecontext.ports.PageRepository;
import com.loremind.infrastructure.persistence.entity.PageJpaEntity;
import com.loremind.infrastructure.persistence.jpa.PageJpaRepository;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Adaptateur d'infrastructure qui implémente le Port PageRepository.
*/
@Repository
public class PostgresPageRepository implements PageRepository {
private final PageJpaRepository jpaRepository;
public PostgresPageRepository(PageJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public Page save(Page page) {
PageJpaEntity jpaEntity = toJpaEntity(page);
PageJpaEntity saved = jpaRepository.save(jpaEntity);
return toDomainEntity(saved);
}
@Override
public Optional<Page> findById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.findById(longId)
.map(this::toDomainEntity);
}
@Override
public List<Page> findByLoreId(String loreId) {
return jpaRepository.findByLoreId(Long.parseLong(loreId)).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public List<Page> findByNodeId(String nodeId) {
Long longNodeId = Long.parseLong(nodeId);
return jpaRepository.findByNodeId(longNodeId).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public List<Page> findByTemplateId(String templateId) {
if (templateId == null || templateId.isEmpty()) {
return List.of();
}
Long longTemplateId = Long.parseLong(templateId);
return jpaRepository.findByTemplateId(longTemplateId).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public List<Page> findAll() {
return jpaRepository.findAll().stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public void deleteById(String id) {
Long longId = Long.parseLong(id);
jpaRepository.deleteById(longId);
}
@Override
public boolean existsById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.existsById(longId);
}
@Override
public long countByLoreId(String loreId) {
return jpaRepository.countByLoreId(Long.parseLong(loreId));
}
@Override
public List<Page> searchByTitle(String query) {
return jpaRepository.findByTitleContainingIgnoreCase(query).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
private Page toDomainEntity(PageJpaEntity e) {
return Page.builder()
.id(e.getId().toString())
.loreId(e.getLoreId() != null ? e.getLoreId().toString() : null)
.nodeId(e.getNodeId() != null ? e.getNodeId().toString() : null)
.templateId(e.getTemplateId() != null ? e.getTemplateId().toString() : null)
.title(e.getTitle())
.values(e.getValues() != null ? new HashMap<>(e.getValues()) : new HashMap<>())
.notes(e.getNotes())
.tags(e.getTags() != null ? new ArrayList<>(e.getTags()) : new ArrayList<>())
.relatedPageIds(e.getRelatedPageIds() != null ? new ArrayList<>(e.getRelatedPageIds()) : new ArrayList<>())
.createdAt(e.getCreatedAt())
.updatedAt(e.getUpdatedAt())
.build();
}
private PageJpaEntity toJpaEntity(Page p) {
return PageJpaEntity.builder()
.id(p.getId() != null ? Long.parseLong(p.getId()) : null)
.loreId(p.getLoreId() != null ? Long.parseLong(p.getLoreId()) : null)
.nodeId(p.getNodeId() != null ? Long.parseLong(p.getNodeId()) : null)
.templateId(p.getTemplateId() != null && !p.getTemplateId().isBlank()
? Long.parseLong(p.getTemplateId()) : null)
.title(p.getTitle())
.values(p.getValues() != null ? new HashMap<>(p.getValues()) : new HashMap<>())
.notes(p.getNotes())
.tags(p.getTags() != null ? new ArrayList<>(p.getTags()) : new ArrayList<>())
.relatedPageIds(p.getRelatedPageIds() != null ? new ArrayList<>(p.getRelatedPageIds()) : new ArrayList<>())
.createdAt(p.getCreatedAt())
.updatedAt(p.getUpdatedAt())
.build();
}
}

View File

@@ -0,0 +1,113 @@
package com.loremind.infrastructure.persistence.postgres;
import com.loremind.domain.campaigncontext.Scene;
import com.loremind.domain.campaigncontext.ports.SceneRepository;
import com.loremind.infrastructure.persistence.entity.SceneJpaEntity;
import com.loremind.infrastructure.persistence.jpa.SceneJpaRepository;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Adaptateur d'infrastructure qui implémente le Port SceneRepository.
*/
@Repository
public class PostgresSceneRepository implements SceneRepository {
private final SceneJpaRepository jpaRepository;
public PostgresSceneRepository(SceneJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public Scene save(Scene scene) {
SceneJpaEntity jpaEntity = toJpaEntity(scene);
SceneJpaEntity saved = jpaRepository.save(jpaEntity);
return toDomainEntity(saved);
}
@Override
public Optional<Scene> findById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.findById(longId)
.map(this::toDomainEntity);
}
@Override
public List<Scene> findByChapterId(String chapterId) {
Long longChapterId = Long.parseLong(chapterId);
return jpaRepository.findByChapterId(longChapterId).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public List<Scene> findAll() {
return jpaRepository.findAll().stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public void deleteById(String id) {
Long longId = Long.parseLong(id);
jpaRepository.deleteById(longId);
}
@Override
public boolean existsById(String id) {
Long longId = Long.parseLong(id);
return jpaRepository.existsById(longId);
}
private Scene toDomainEntity(SceneJpaEntity jpaEntity) {
return Scene.builder()
.id(jpaEntity.getId().toString())
.name(jpaEntity.getName())
.description(jpaEntity.getDescription())
.chapterId(jpaEntity.getChapterId().toString())
.order(jpaEntity.getOrder())
.location(jpaEntity.getLocation())
.timing(jpaEntity.getTiming())
.atmosphere(jpaEntity.getAtmosphere())
.playerNarration(jpaEntity.getPlayerNarration())
.gmSecretNotes(jpaEntity.getGmSecretNotes())
.choicesConsequences(jpaEntity.getChoicesConsequences())
.combatDifficulty(jpaEntity.getCombatDifficulty())
.enemies(jpaEntity.getEnemies())
.relatedPageIds(jpaEntity.getRelatedPageIds() != null
? new ArrayList<>(jpaEntity.getRelatedPageIds())
: new ArrayList<>())
.createdAt(jpaEntity.getCreatedAt())
.updatedAt(jpaEntity.getUpdatedAt())
.build();
}
private SceneJpaEntity toJpaEntity(Scene scene) {
Long id = scene.getId() != null ? Long.parseLong(scene.getId()) : null;
return SceneJpaEntity.builder()
.id(id)
.name(scene.getName())
.description(scene.getDescription())
.chapterId(Long.parseLong(scene.getChapterId()))
.order(scene.getOrder())
.location(scene.getLocation())
.timing(scene.getTiming())
.atmosphere(scene.getAtmosphere())
.playerNarration(scene.getPlayerNarration())
.gmSecretNotes(scene.getGmSecretNotes())
.choicesConsequences(scene.getChoicesConsequences())
.combatDifficulty(scene.getCombatDifficulty())
.enemies(scene.getEnemies())
.relatedPageIds(scene.getRelatedPageIds() != null
? new ArrayList<>(scene.getRelatedPageIds())
: new ArrayList<>())
.createdAt(scene.getCreatedAt())
.updatedAt(scene.getUpdatedAt())
.build();
}
}

View File

@@ -0,0 +1,97 @@
package com.loremind.infrastructure.persistence.postgres;
import com.loremind.domain.lorecontext.Template;
import com.loremind.domain.lorecontext.ports.TemplateRepository;
import com.loremind.infrastructure.persistence.entity.TemplateJpaEntity;
import com.loremind.infrastructure.persistence.jpa.TemplateJpaRepository;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Adaptateur d'infrastructure qui implémente le Port TemplateRepository.
* Responsable du mapping bidirectionnel entre l'entité de domaine (Template)
* et l'entité JPA (TemplateJpaEntity).
*/
@Repository
public class PostgresTemplateRepository implements TemplateRepository {
private final TemplateJpaRepository jpaRepository;
public PostgresTemplateRepository(TemplateJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public Template save(Template template) {
TemplateJpaEntity saved = jpaRepository.save(toJpaEntity(template));
return toDomainEntity(saved);
}
@Override
public Optional<Template> findById(String id) {
return jpaRepository.findById(Long.parseLong(id)).map(this::toDomainEntity);
}
@Override
public List<Template> findAll() {
return jpaRepository.findAll().stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public List<Template> findByLoreId(String loreId) {
return jpaRepository.findByLoreId(Long.parseLong(loreId)).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
@Override
public void deleteById(String id) {
jpaRepository.deleteById(Long.parseLong(id));
}
@Override
public boolean existsById(String id) {
return jpaRepository.existsById(Long.parseLong(id));
}
@Override
public List<Template> searchByName(String query) {
return jpaRepository.findByNameContainingIgnoreCase(query).stream()
.map(this::toDomainEntity)
.collect(Collectors.toList());
}
// --- Mapping ----------------------------------------------------------
private Template toDomainEntity(TemplateJpaEntity e) {
return Template.builder()
.id(e.getId().toString())
.loreId(e.getLoreId() != null ? e.getLoreId().toString() : null)
.name(e.getName())
.description(e.getDescription())
.defaultNodeId(e.getDefaultNodeId() != null ? e.getDefaultNodeId().toString() : null)
.fields(e.getFields() != null ? new ArrayList<>(e.getFields()) : new ArrayList<>())
.createdAt(e.getCreatedAt())
.updatedAt(e.getUpdatedAt())
.build();
}
private TemplateJpaEntity toJpaEntity(Template t) {
return TemplateJpaEntity.builder()
.id(t.getId() != null ? Long.parseLong(t.getId()) : null)
.loreId(t.getLoreId() != null ? Long.parseLong(t.getLoreId()) : null)
.name(t.getName())
.description(t.getDescription())
.defaultNodeId(t.getDefaultNodeId() != null ? Long.parseLong(t.getDefaultNodeId()) : null)
.fields(t.getFields() != null ? new ArrayList<>(t.getFields()) : new ArrayList<>())
.createdAt(t.getCreatedAt())
.updatedAt(t.getUpdatedAt())
.build();
}
}

View File

@@ -0,0 +1,29 @@
package com.loremind.infrastructure.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* Configuration CORS pour autoriser les requêtes depuis le Frontend Angular.
* Adaptateur d'infrastructure qui configure la politique CORS.
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// Autoriser les requêtes depuis localhost:4200 (Angular dev server)
config.addAllowedOrigin("http://localhost:4200");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@@ -0,0 +1,71 @@
package com.loremind.infrastructure.web.controller;
import com.loremind.application.campaigncontext.ArcService;
import com.loremind.domain.campaigncontext.Arc;
import com.loremind.infrastructure.web.dto.campaigncontext.ArcDTO;
import com.loremind.infrastructure.web.mapper.ArcMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* REST Controller pour le contexte Arc.
*/
@RestController
@RequestMapping("/api/arcs")
public class ArcController {
private final ArcService arcService;
private final ArcMapper arcMapper;
public ArcController(ArcService arcService, ArcMapper arcMapper) {
this.arcService = arcService;
this.arcMapper = arcMapper;
}
@PostMapping
public ResponseEntity<ArcDTO> createArc(@RequestBody ArcDTO arcDTO) {
Arc arc = arcMapper.toDomain(arcDTO);
Arc createdArc = arcService.createArc(arc.getName(), arc.getDescription(), arc.getCampaignId(), arc.getOrder());
return ResponseEntity.ok(arcMapper.toDTO(createdArc));
}
@GetMapping("/{id}")
public ResponseEntity<ArcDTO> getArcById(@PathVariable String id) {
return arcService.getArcById(id)
.map(arc -> ResponseEntity.ok(arcMapper.toDTO(arc)))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<ArcDTO>> getAllArcs() {
List<Arc> arcs = arcService.getAllArcs();
List<ArcDTO> arcDTOs = arcs.stream()
.map(arcMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(arcDTOs);
}
@GetMapping("/campaign/{campaignId}")
public ResponseEntity<List<ArcDTO>> getArcsByCampaignId(@PathVariable String campaignId) {
List<Arc> arcs = arcService.getArcsByCampaignId(campaignId);
List<ArcDTO> arcDTOs = arcs.stream()
.map(arcMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(arcDTOs);
}
@PutMapping("/{id}")
public ResponseEntity<ArcDTO> updateArc(@PathVariable String id, @RequestBody ArcDTO arcDTO) {
Arc updatedArc = arcService.updateArc(id, arcMapper.toDomain(arcDTO));
return ResponseEntity.ok(arcMapper.toDTO(updatedArc));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteArc(@PathVariable String id) {
arcService.deleteArc(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,77 @@
package com.loremind.infrastructure.web.controller;
import com.loremind.application.campaigncontext.CampaignService;
import com.loremind.domain.campaigncontext.Campaign;
import com.loremind.infrastructure.web.dto.campaigncontext.CampaignDTO;
import com.loremind.infrastructure.web.mapper.CampaignMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* REST Controller pour le contexte Campaign.
* Adaptateur d'infrastructure qui expose l'API REST.
* Utilise le Service d'application et le Mapper selon l'Architecture Hexagonale.
*/
@RestController
@RequestMapping("/api/campaigns")
public class CampaignController {
private final CampaignService campaignService;
private final CampaignMapper campaignMapper;
public CampaignController(CampaignService campaignService, CampaignMapper campaignMapper) {
this.campaignService = campaignService;
this.campaignMapper = campaignMapper;
}
@PostMapping
public ResponseEntity<CampaignDTO> createCampaign(@RequestBody CampaignDTO campaignDTO) {
Campaign campaign = campaignMapper.toDomain(campaignDTO);
Campaign createdCampaign = campaignService.createCampaign(
new CampaignService.CampaignData(campaign.getName(), campaign.getDescription(), campaign.getLoreId())
);
return ResponseEntity.ok(campaignMapper.toDTO(createdCampaign));
}
@GetMapping("/{id}")
public ResponseEntity<CampaignDTO> getCampaignById(@PathVariable String id) {
return campaignService.getCampaignById(id)
.map(campaign -> ResponseEntity.ok(campaignMapper.toDTO(campaign)))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<CampaignDTO>> getAllCampaigns() {
List<Campaign> campaigns = campaignService.getAllCampaigns();
List<CampaignDTO> campaignDTOs = campaigns.stream()
.map(campaignMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(campaignDTOs);
}
@GetMapping("/search")
public ResponseEntity<List<CampaignDTO>> searchCampaigns(@RequestParam("q") String query) {
List<CampaignDTO> dtos = campaignService.searchCampaigns(query).stream()
.map(campaignMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
@PutMapping("/{id}")
public ResponseEntity<CampaignDTO> updateCampaign(@PathVariable String id, @RequestBody CampaignDTO campaignDTO) {
Campaign updatedCampaign = campaignService.updateCampaign(
id,
new CampaignService.CampaignData(campaignDTO.getName(), campaignDTO.getDescription(), campaignDTO.getLoreId())
);
return ResponseEntity.ok(campaignMapper.toDTO(updatedCampaign));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCampaign(@PathVariable String id) {
campaignService.deleteCampaign(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,71 @@
package com.loremind.infrastructure.web.controller;
import com.loremind.application.campaigncontext.ChapterService;
import com.loremind.domain.campaigncontext.Chapter;
import com.loremind.infrastructure.web.dto.campaigncontext.ChapterDTO;
import com.loremind.infrastructure.web.mapper.ChapterMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* REST Controller pour le contexte Chapter.
*/
@RestController
@RequestMapping("/api/chapters")
public class ChapterController {
private final ChapterService chapterService;
private final ChapterMapper chapterMapper;
public ChapterController(ChapterService chapterService, ChapterMapper chapterMapper) {
this.chapterService = chapterService;
this.chapterMapper = chapterMapper;
}
@PostMapping
public ResponseEntity<ChapterDTO> createChapter(@RequestBody ChapterDTO chapterDTO) {
Chapter chapter = chapterMapper.toDomain(chapterDTO);
Chapter createdChapter = chapterService.createChapter(chapter.getName(), chapter.getDescription(), chapter.getArcId(), chapter.getOrder());
return ResponseEntity.ok(chapterMapper.toDTO(createdChapter));
}
@GetMapping("/{id}")
public ResponseEntity<ChapterDTO> getChapterById(@PathVariable String id) {
return chapterService.getChapterById(id)
.map(chapter -> ResponseEntity.ok(chapterMapper.toDTO(chapter)))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<ChapterDTO>> getAllChapters() {
List<Chapter> chapters = chapterService.getAllChapters();
List<ChapterDTO> chapterDTOs = chapters.stream()
.map(chapterMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(chapterDTOs);
}
@GetMapping("/arc/{arcId}")
public ResponseEntity<List<ChapterDTO>> getChaptersByArcId(@PathVariable String arcId) {
List<Chapter> chapters = chapterService.getChaptersByArcId(arcId);
List<ChapterDTO> chapterDTOs = chapters.stream()
.map(chapterMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(chapterDTOs);
}
@PutMapping("/{id}")
public ResponseEntity<ChapterDTO> updateChapter(@PathVariable String id, @RequestBody ChapterDTO chapterDTO) {
Chapter updatedChapter = chapterService.updateChapter(id, chapterMapper.toDomain(chapterDTO));
return ResponseEntity.ok(chapterMapper.toDTO(updatedChapter));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteChapter(@PathVariable String id) {
chapterService.deleteChapter(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,72 @@
package com.loremind.infrastructure.web.controller;
import com.loremind.application.lorecontext.LoreService;
import com.loremind.domain.lorecontext.Lore;
import com.loremind.infrastructure.web.dto.lorecontext.LoreDTO;
import com.loremind.infrastructure.web.mapper.LoreMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* REST Controller pour le contexte Lore.
* Adaptateur d'infrastructure qui expose l'API REST.
* Utilise le Service d'application et le Mapper selon l'Architecture Hexagonale.
*/
@RestController
@RequestMapping("/api/lores")
public class LoreController {
private final LoreService loreService;
private final LoreMapper loreMapper;
public LoreController(LoreService loreService, LoreMapper loreMapper) {
this.loreService = loreService;
this.loreMapper = loreMapper;
}
@PostMapping
public ResponseEntity<LoreDTO> createLore(@RequestBody LoreDTO loreDTO) {
Lore lore = loreMapper.toDomain(loreDTO);
Lore createdLore = loreService.createLore(lore.getName(), lore.getDescription());
return ResponseEntity.ok(loreMapper.toDTO(createdLore));
}
@GetMapping("/{id}")
public ResponseEntity<LoreDTO> getLoreById(@PathVariable String id) {
return loreService.getLoreById(id)
.map(lore -> ResponseEntity.ok(loreMapper.toDTO(lore)))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<LoreDTO>> getAllLores() {
List<Lore> lores = loreService.getAllLores();
List<LoreDTO> loreDTOs = lores.stream()
.map(loreMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(loreDTOs);
}
@GetMapping("/search")
public ResponseEntity<List<LoreDTO>> searchLores(@RequestParam("q") String query) {
List<LoreDTO> dtos = loreService.searchLores(query).stream()
.map(loreMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
@PutMapping("/{id}")
public ResponseEntity<LoreDTO> updateLore(@PathVariable String id, @RequestBody LoreDTO loreDTO) {
Lore updatedLore = loreService.updateLore(id, loreDTO.getName(), loreDTO.getDescription());
return ResponseEntity.ok(loreMapper.toDTO(updatedLore));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteLore(@PathVariable String id) {
loreService.deleteLore(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,100 @@
package com.loremind.infrastructure.web.controller;
import com.loremind.application.lorecontext.LoreNodeService;
import com.loremind.domain.lorecontext.LoreNode;
import com.loremind.infrastructure.web.dto.lorecontext.LoreNodeDTO;
import com.loremind.infrastructure.web.mapper.LoreNodeMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* REST Controller pour le contexte LoreNode.
*/
@RestController
@RequestMapping("/api/lore-nodes")
public class LoreNodeController {
private final LoreNodeService loreNodeService;
private final LoreNodeMapper loreNodeMapper;
public LoreNodeController(LoreNodeService loreNodeService, LoreNodeMapper loreNodeMapper) {
this.loreNodeService = loreNodeService;
this.loreNodeMapper = loreNodeMapper;
}
@PostMapping
public ResponseEntity<LoreNodeDTO> createLoreNode(@RequestBody LoreNodeDTO loreNodeDTO) {
LoreNode changes = loreNodeMapper.toDomain(loreNodeDTO);
LoreNode created = loreNodeService.createLoreNode(changes);
return ResponseEntity.ok(loreNodeMapper.toDTO(created));
}
@GetMapping("/{id}")
public ResponseEntity<LoreNodeDTO> getLoreNodeById(@PathVariable String id) {
return loreNodeService.getLoreNodeById(id)
.map(loreNode -> ResponseEntity.ok(loreNodeMapper.toDTO(loreNode)))
.orElse(ResponseEntity.notFound().build());
}
/**
* GET /api/lore-nodes → tous les dossiers (usage admin/debug)
* GET /api/lore-nodes?loreId=X → uniquement les dossiers du Lore X
*
* Sans ce filtre, le frontend recevait TOUS les dossiers de TOUS les lores,
* ce qui polluait l'affichage quand on switche entre deux lores.
* Pattern aligné sur TemplateController et PageController.
*/
@GetMapping
public ResponseEntity<List<LoreNodeDTO>> getLoreNodes(
@RequestParam(value = "loreId", required = false) String loreId) {
List<LoreNode> loreNodes = (loreId != null && !loreId.isBlank())
? loreNodeService.getLoreNodesByLoreId(loreId)
: loreNodeService.getAllLoreNodes();
List<LoreNodeDTO> loreNodeDTOs = loreNodes.stream()
.map(loreNodeMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(loreNodeDTOs);
}
@GetMapping("/search")
public ResponseEntity<List<LoreNodeDTO>> searchLoreNodes(@RequestParam("q") String query) {
List<LoreNodeDTO> dtos = loreNodeService.searchLoreNodes(query).stream()
.map(loreNodeMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
@GetMapping("/lore/{loreId}")
public ResponseEntity<List<LoreNodeDTO>> getLoreNodesByLoreId(@PathVariable String loreId) {
List<LoreNode> loreNodes = loreNodeService.getLoreNodesByLoreId(loreId);
List<LoreNodeDTO> loreNodeDTOs = loreNodes.stream()
.map(loreNodeMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(loreNodeDTOs);
}
@GetMapping("/parent/{parentId}")
public ResponseEntity<List<LoreNodeDTO>> getLoreNodesByParentId(@PathVariable String parentId) {
List<LoreNode> loreNodes = loreNodeService.getLoreNodesByParentId(parentId);
List<LoreNodeDTO> loreNodeDTOs = loreNodes.stream()
.map(loreNodeMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(loreNodeDTOs);
}
@PutMapping("/{id}")
public ResponseEntity<LoreNodeDTO> updateLoreNode(@PathVariable String id, @RequestBody LoreNodeDTO loreNodeDTO) {
LoreNode changes = loreNodeMapper.toDomain(loreNodeDTO);
LoreNode updated = loreNodeService.updateLoreNode(id, changes);
return ResponseEntity.ok(loreNodeMapper.toDTO(updated));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteLoreNode(@PathVariable String id) {
loreNodeService.deleteLoreNode(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,89 @@
package com.loremind.infrastructure.web.controller;
import com.loremind.application.lorecontext.PageService;
import com.loremind.domain.lorecontext.Page;
import com.loremind.infrastructure.web.dto.lorecontext.PageDTO;
import com.loremind.infrastructure.web.mapper.PageMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* REST Controller pour le contexte Page.
* - GET /api/pages?loreId=X → toutes les pages d'un lore
* - GET /api/pages?nodeId=Y → toutes les pages d'un noeud
* - GET /api/pages/{id} → détail
* - POST /api/pages → création (loreId, nodeId, templateId, title)
* - PUT /api/pages/{id} → mise à jour complète (title, nodeId, values, notes, tags, relatedPageIds)
* - DELETE /api/pages/{id} → suppression
*/
@RestController
@RequestMapping("/api/pages")
public class PageController {
private final PageService pageService;
private final PageMapper pageMapper;
public PageController(PageService pageService, PageMapper pageMapper) {
this.pageService = pageService;
this.pageMapper = pageMapper;
}
@PostMapping
public ResponseEntity<PageDTO> createPage(@RequestBody PageDTO dto) {
Page created = pageService.createPage(
dto.getLoreId(),
dto.getNodeId(),
dto.getTemplateId(),
dto.getTitle()
);
return ResponseEntity.ok(pageMapper.toDTO(created));
}
@GetMapping("/{id}")
public ResponseEntity<PageDTO> getPageById(@PathVariable String id) {
return pageService.getPageById(id)
.map(page -> ResponseEntity.ok(pageMapper.toDTO(page)))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<PageDTO>> getPages(
@RequestParam(value = "loreId", required = false) String loreId,
@RequestParam(value = "nodeId", required = false) String nodeId) {
List<Page> pages;
if (loreId != null && !loreId.isBlank()) {
pages = pageService.getPagesByLoreId(loreId);
} else if (nodeId != null && !nodeId.isBlank()) {
pages = pageService.getPagesByNodeId(nodeId);
} else {
pages = pageService.getAllPages();
}
List<PageDTO> dtos = pages.stream().map(pageMapper::toDTO).collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
@GetMapping("/search")
public ResponseEntity<List<PageDTO>> searchPages(@RequestParam("q") String query) {
List<PageDTO> dtos = pageService.searchPages(query).stream()
.map(pageMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
@PutMapping("/{id}")
public ResponseEntity<PageDTO> updatePage(@PathVariable String id,
@RequestBody PageDTO dto) {
Page changes = pageMapper.toDomain(dto);
Page updated = pageService.updatePage(id, changes);
return ResponseEntity.ok(pageMapper.toDTO(updated));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePage(@PathVariable String id) {
pageService.deletePage(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,71 @@
package com.loremind.infrastructure.web.controller;
import com.loremind.application.campaigncontext.SceneService;
import com.loremind.domain.campaigncontext.Scene;
import com.loremind.infrastructure.web.dto.campaigncontext.SceneDTO;
import com.loremind.infrastructure.web.mapper.SceneMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* REST Controller pour le contexte Scene.
*/
@RestController
@RequestMapping("/api/scenes")
public class SceneController {
private final SceneService sceneService;
private final SceneMapper sceneMapper;
public SceneController(SceneService sceneService, SceneMapper sceneMapper) {
this.sceneService = sceneService;
this.sceneMapper = sceneMapper;
}
@PostMapping
public ResponseEntity<SceneDTO> createScene(@RequestBody SceneDTO sceneDTO) {
Scene scene = sceneMapper.toDomain(sceneDTO);
Scene createdScene = sceneService.createScene(scene.getName(), scene.getDescription(), scene.getChapterId(), scene.getOrder());
return ResponseEntity.ok(sceneMapper.toDTO(createdScene));
}
@GetMapping("/{id}")
public ResponseEntity<SceneDTO> getSceneById(@PathVariable String id) {
return sceneService.getSceneById(id)
.map(scene -> ResponseEntity.ok(sceneMapper.toDTO(scene)))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<SceneDTO>> getAllScenes() {
List<Scene> scenes = sceneService.getAllScenes();
List<SceneDTO> sceneDTOs = scenes.stream()
.map(sceneMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(sceneDTOs);
}
@GetMapping("/chapter/{chapterId}")
public ResponseEntity<List<SceneDTO>> getScenesByChapterId(@PathVariable String chapterId) {
List<Scene> scenes = sceneService.getScenesByChapterId(chapterId);
List<SceneDTO> sceneDTOs = scenes.stream()
.map(sceneMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(sceneDTOs);
}
@PutMapping("/{id}")
public ResponseEntity<SceneDTO> updateScene(@PathVariable String id, @RequestBody SceneDTO sceneDTO) {
Scene updatedScene = sceneService.updateScene(id, sceneMapper.toDomain(sceneDTO));
return ResponseEntity.ok(sceneMapper.toDTO(updatedScene));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteScene(@PathVariable String id) {
sceneService.deleteScene(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,82 @@
package com.loremind.infrastructure.web.controller;
import com.loremind.application.lorecontext.TemplateService;
import com.loremind.domain.lorecontext.Template;
import com.loremind.infrastructure.web.dto.lorecontext.TemplateDTO;
import com.loremind.infrastructure.web.mapper.TemplateMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* REST Controller pour le contexte Template.
* Expose également un endpoint filtré par Lore pour alimenter le panneau
* "Templates" du sidebar secondaire.
*/
@RestController
@RequestMapping("/api/templates")
public class TemplateController {
private final TemplateService templateService;
private final TemplateMapper templateMapper;
public TemplateController(TemplateService templateService, TemplateMapper templateMapper) {
this.templateService = templateService;
this.templateMapper = templateMapper;
}
@PostMapping
public ResponseEntity<TemplateDTO> createTemplate(@RequestBody TemplateDTO dto) {
Template created = templateService.createTemplate(
dto.getLoreId(),
dto.getName(),
dto.getDescription(),
dto.getDefaultNodeId(),
dto.getFields()
);
return ResponseEntity.ok(templateMapper.toDTO(created));
}
@GetMapping("/{id}")
public ResponseEntity<TemplateDTO> getTemplateById(@PathVariable String id) {
return templateService.getTemplateById(id)
.map(template -> ResponseEntity.ok(templateMapper.toDTO(template)))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<TemplateDTO>> getAllTemplates(
@RequestParam(value = "loreId", required = false) String loreId) {
List<Template> templates = (loreId != null && !loreId.isBlank())
? templateService.getTemplatesByLoreId(loreId)
: templateService.getAllTemplates();
List<TemplateDTO> dtos = templates.stream()
.map(templateMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
@GetMapping("/search")
public ResponseEntity<List<TemplateDTO>> searchTemplates(@RequestParam("q") String query) {
List<TemplateDTO> dtos = templateService.searchTemplates(query).stream()
.map(templateMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
@PutMapping("/{id}")
public ResponseEntity<TemplateDTO> updateTemplate(@PathVariable String id,
@RequestBody TemplateDTO dto) {
Template changes = templateMapper.toDomain(dto);
Template updated = templateService.updateTemplate(id, changes);
return ResponseEntity.ok(templateMapper.toDTO(updated));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTemplate(@PathVariable String id) {
templateService.deleteTemplate(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,29 @@
package com.loremind.infrastructure.web.dto.campaigncontext;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* DTO pour l'entité Arc.
* Objet de transfert de données pour l'API REST.
*/
@Data
public class ArcDTO {
private String id;
private String name;
private String description;
private String campaignId;
private int order;
// Champs narratifs enrichis
private String themes;
private String stakes;
private String gmNotes;
private String rewards;
private String resolution;
/** IDs des pages du Lore liées à cet arc (weak cross-context references). */
private List<String> relatedPageIds = new ArrayList<>();
}

View File

@@ -0,0 +1,18 @@
package com.loremind.infrastructure.web.dto.campaigncontext;
import lombok.Data;
/**
* DTO pour l'entité Campaign.
* Objet de transfert de données pour l'API REST.
*/
@Data
public class CampaignDTO {
private String id;
private String name;
private String description;
private int arcsCount;
/** Nullable : campagne sans univers associé. */
private String loreId;
}

View File

@@ -0,0 +1,27 @@
package com.loremind.infrastructure.web.dto.campaigncontext;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* DTO pour l'entité Chapter.
* Objet de transfert de données pour l'API REST.
*/
@Data
public class ChapterDTO {
private String id;
private String name;
private String description;
private String arcId;
private int order;
// Champs narratifs enrichis
private String gmNotes;
private String playerObjectives;
private String narrativeStakes;
/** IDs des pages du Lore liées (weak cross-context references). */
private List<String> relatedPageIds = new ArrayList<>();
}

View File

@@ -0,0 +1,32 @@
package com.loremind.infrastructure.web.dto.campaigncontext;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* DTO pour l'entité Scene.
* Objet de transfert de données pour l'API REST.
*/
@Data
public class SceneDTO {
private String id;
private String name;
private String description;
private String chapterId;
private int order;
// Champs narratifs enrichis
private String location;
private String timing;
private String atmosphere;
private String playerNarration;
private String gmSecretNotes;
private String choicesConsequences;
private String combatDifficulty;
private String enemies;
/** IDs des pages du Lore liées (weak cross-context references). */
private List<String> relatedPageIds = new ArrayList<>();
}

View File

@@ -0,0 +1,18 @@
package com.loremind.infrastructure.web.dto.lorecontext;
import lombok.Data;
/**
* DTO pour l'entité Lore.
* Objet de transfert de données pour l'API REST.
* Contient uniquement les champs nécessaires pour le Frontend.
*/
@Data
public class LoreDTO {
private String id;
private String name;
private String description;
private int nodeCount;
private int pageCount;
}

View File

@@ -0,0 +1,17 @@
package com.loremind.infrastructure.web.dto.lorecontext;
import lombok.Data;
/**
* DTO pour l'entité LoreNode.
* Objet de transfert de données pour l'API REST.
*/
@Data
public class LoreNodeDTO {
private String id;
private String name;
private String icon;
private String parentId;
private String loreId;
}

View File

@@ -0,0 +1,24 @@
package com.loremind.infrastructure.web.dto.lorecontext;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* DTO pour l'entité Page.
* Objet de transfert de données pour l'API REST.
*/
@Data
public class PageDTO {
private String id;
private String loreId;
private String nodeId;
private String templateId;
private String title;
private Map<String, String> values;
private String notes;
private List<String> tags;
private List<String> relatedPageIds;
}

View File

@@ -0,0 +1,22 @@
package com.loremind.infrastructure.web.dto.lorecontext;
import lombok.Data;
import java.util.List;
/**
* DTO pour l'entité Template.
* Objet de transfert de données pour l'API REST.
* Expose un compteur fieldCount pour éviter aux clients de recalculer fields.size().
*/
@Data
public class TemplateDTO {
private String id;
private String loreId;
private String name;
private String description;
private String defaultNodeId;
private List<String> fields;
private int fieldCount;
}

View File

@@ -0,0 +1,58 @@
package com.loremind.infrastructure.web.mapper;
import com.loremind.domain.campaigncontext.Arc;
import com.loremind.infrastructure.web.dto.campaigncontext.ArcDTO;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/**
* Mapper pour convertir entre Arc (entité de domaine) et ArcDTO.
*/
@Component
public class ArcMapper {
public ArcDTO toDTO(Arc arc) {
if (arc == null) {
return null;
}
ArcDTO dto = new ArcDTO();
dto.setId(arc.getId());
dto.setName(arc.getName());
dto.setDescription(arc.getDescription());
dto.setCampaignId(arc.getCampaignId());
dto.setOrder(arc.getOrder());
dto.setThemes(arc.getThemes());
dto.setStakes(arc.getStakes());
dto.setGmNotes(arc.getGmNotes());
dto.setRewards(arc.getRewards());
dto.setResolution(arc.getResolution());
dto.setRelatedPageIds(arc.getRelatedPageIds() != null
? new ArrayList<>(arc.getRelatedPageIds())
: new ArrayList<>());
return dto;
}
public Arc toDomain(ArcDTO dto) {
if (dto == null) {
return null;
}
return Arc.builder()
.id(dto.getId())
.name(dto.getName())
.description(dto.getDescription())
.campaignId(dto.getCampaignId())
.order(dto.getOrder())
.themes(dto.getThemes())
.stakes(dto.getStakes())
.gmNotes(dto.getGmNotes())
.rewards(dto.getRewards())
.resolution(dto.getResolution())
.relatedPageIds(dto.getRelatedPageIds() != null
? new ArrayList<>(dto.getRelatedPageIds())
: new ArrayList<>())
.build();
}
}

View File

@@ -0,0 +1,40 @@
package com.loremind.infrastructure.web.mapper;
import com.loremind.domain.campaigncontext.Campaign;
import com.loremind.infrastructure.web.dto.campaigncontext.CampaignDTO;
import org.springframework.stereotype.Component;
/**
* Mapper pour convertir entre Campaign (entité de domaine) et CampaignDTO.
*/
@Component
public class CampaignMapper {
public CampaignDTO toDTO(Campaign campaign) {
if (campaign == null) {
return null;
}
CampaignDTO dto = new CampaignDTO();
dto.setId(campaign.getId());
dto.setName(campaign.getName());
dto.setDescription(campaign.getDescription());
dto.setArcsCount(campaign.getArcsCount());
dto.setLoreId(campaign.getLoreId());
return dto;
}
public Campaign toDomain(CampaignDTO dto) {
if (dto == null) {
return null;
}
return Campaign.builder()
.id(dto.getId())
.name(dto.getName())
.description(dto.getDescription())
.arcsCount(dto.getArcsCount())
.loreId(dto.getLoreId())
.build();
}
}

View File

@@ -0,0 +1,54 @@
package com.loremind.infrastructure.web.mapper;
import com.loremind.domain.campaigncontext.Chapter;
import com.loremind.infrastructure.web.dto.campaigncontext.ChapterDTO;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/**
* Mapper pour convertir entre Chapter (entité de domaine) et ChapterDTO.
*/
@Component
public class ChapterMapper {
public ChapterDTO toDTO(Chapter chapter) {
if (chapter == null) {
return null;
}
ChapterDTO dto = new ChapterDTO();
dto.setId(chapter.getId());
dto.setName(chapter.getName());
dto.setDescription(chapter.getDescription());
dto.setArcId(chapter.getArcId());
dto.setOrder(chapter.getOrder());
dto.setGmNotes(chapter.getGmNotes());
dto.setPlayerObjectives(chapter.getPlayerObjectives());
dto.setNarrativeStakes(chapter.getNarrativeStakes());
dto.setRelatedPageIds(chapter.getRelatedPageIds() != null
? new ArrayList<>(chapter.getRelatedPageIds())
: new ArrayList<>());
return dto;
}
public Chapter toDomain(ChapterDTO dto) {
if (dto == null) {
return null;
}
return Chapter.builder()
.id(dto.getId())
.name(dto.getName())
.description(dto.getDescription())
.arcId(dto.getArcId())
.order(dto.getOrder())
.gmNotes(dto.getGmNotes())
.playerObjectives(dto.getPlayerObjectives())
.narrativeStakes(dto.getNarrativeStakes())
.relatedPageIds(dto.getRelatedPageIds() != null
? new ArrayList<>(dto.getRelatedPageIds())
: new ArrayList<>())
.build();
}
}

View File

@@ -0,0 +1,41 @@
package com.loremind.infrastructure.web.mapper;
import com.loremind.domain.lorecontext.Lore;
import com.loremind.infrastructure.web.dto.lorecontext.LoreDTO;
import org.springframework.stereotype.Component;
/**
* Mapper pour convertir entre Lore (entité de domaine) et LoreDTO.
* Fait partie de la couche Infrastructure de l'Architecture Hexagonale.
*/
@Component
public class LoreMapper {
public LoreDTO toDTO(Lore lore) {
if (lore == null) {
return null;
}
LoreDTO dto = new LoreDTO();
dto.setId(lore.getId());
dto.setName(lore.getName());
dto.setDescription(lore.getDescription());
dto.setNodeCount(lore.getNodeCount());
dto.setPageCount(lore.getPageCount());
return dto;
}
public Lore toDomain(LoreDTO dto) {
if (dto == null) {
return null;
}
return Lore.builder()
.id(dto.getId())
.name(dto.getName())
.description(dto.getDescription())
.nodeCount(dto.getNodeCount())
.pageCount(dto.getPageCount())
.build();
}
}

View File

@@ -0,0 +1,40 @@
package com.loremind.infrastructure.web.mapper;
import com.loremind.domain.lorecontext.LoreNode;
import com.loremind.infrastructure.web.dto.lorecontext.LoreNodeDTO;
import org.springframework.stereotype.Component;
/**
* Mapper pour convertir entre LoreNode (entité de domaine) et LoreNodeDTO.
*/
@Component
public class LoreNodeMapper {
public LoreNodeDTO toDTO(LoreNode loreNode) {
if (loreNode == null) {
return null;
}
LoreNodeDTO dto = new LoreNodeDTO();
dto.setId(loreNode.getId());
dto.setName(loreNode.getName());
dto.setIcon(loreNode.getIcon());
dto.setParentId(loreNode.getParentId());
dto.setLoreId(loreNode.getLoreId());
return dto;
}
public LoreNode toDomain(LoreNodeDTO dto) {
if (dto == null) {
return null;
}
return LoreNode.builder()
.id(dto.getId())
.name(dto.getName())
.icon(dto.getIcon())
.parentId(dto.getParentId())
.loreId(dto.getLoreId())
.build();
}
}

View File

@@ -0,0 +1,49 @@
package com.loremind.infrastructure.web.mapper;
import com.loremind.domain.lorecontext.Page;
import com.loremind.infrastructure.web.dto.lorecontext.PageDTO;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Mapper pour convertir entre Page (entité de domaine) et PageDTO.
*/
@Component
public class PageMapper {
public PageDTO toDTO(Page page) {
if (page == null) {
return null;
}
PageDTO dto = new PageDTO();
dto.setId(page.getId());
dto.setLoreId(page.getLoreId());
dto.setNodeId(page.getNodeId());
dto.setTemplateId(page.getTemplateId());
dto.setTitle(page.getTitle());
dto.setValues(page.getValues() != null ? new HashMap<>(page.getValues()) : new HashMap<>());
dto.setNotes(page.getNotes());
dto.setTags(page.getTags() != null ? new ArrayList<>(page.getTags()) : new ArrayList<>());
dto.setRelatedPageIds(page.getRelatedPageIds() != null ? new ArrayList<>(page.getRelatedPageIds()) : new ArrayList<>());
return dto;
}
public Page toDomain(PageDTO dto) {
if (dto == null) {
return null;
}
return Page.builder()
.id(dto.getId())
.loreId(dto.getLoreId())
.nodeId(dto.getNodeId())
.templateId(dto.getTemplateId())
.title(dto.getTitle())
.values(dto.getValues() != null ? new HashMap<>(dto.getValues()) : new HashMap<>())
.notes(dto.getNotes())
.tags(dto.getTags() != null ? new ArrayList<>(dto.getTags()) : new ArrayList<>())
.relatedPageIds(dto.getRelatedPageIds() != null ? new ArrayList<>(dto.getRelatedPageIds()) : new ArrayList<>())
.build();
}
}

View File

@@ -0,0 +1,64 @@
package com.loremind.infrastructure.web.mapper;
import com.loremind.domain.campaigncontext.Scene;
import com.loremind.infrastructure.web.dto.campaigncontext.SceneDTO;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/**
* Mapper pour convertir entre Scene (entité de domaine) et SceneDTO.
*/
@Component
public class SceneMapper {
public SceneDTO toDTO(Scene scene) {
if (scene == null) {
return null;
}
SceneDTO dto = new SceneDTO();
dto.setId(scene.getId());
dto.setName(scene.getName());
dto.setDescription(scene.getDescription());
dto.setChapterId(scene.getChapterId());
dto.setOrder(scene.getOrder());
dto.setLocation(scene.getLocation());
dto.setTiming(scene.getTiming());
dto.setAtmosphere(scene.getAtmosphere());
dto.setPlayerNarration(scene.getPlayerNarration());
dto.setGmSecretNotes(scene.getGmSecretNotes());
dto.setChoicesConsequences(scene.getChoicesConsequences());
dto.setCombatDifficulty(scene.getCombatDifficulty());
dto.setEnemies(scene.getEnemies());
dto.setRelatedPageIds(scene.getRelatedPageIds() != null
? new ArrayList<>(scene.getRelatedPageIds())
: new ArrayList<>());
return dto;
}
public Scene toDomain(SceneDTO dto) {
if (dto == null) {
return null;
}
return Scene.builder()
.id(dto.getId())
.name(dto.getName())
.description(dto.getDescription())
.chapterId(dto.getChapterId())
.order(dto.getOrder())
.location(dto.getLocation())
.timing(dto.getTiming())
.atmosphere(dto.getAtmosphere())
.playerNarration(dto.getPlayerNarration())
.gmSecretNotes(dto.getGmSecretNotes())
.choicesConsequences(dto.getChoicesConsequences())
.combatDifficulty(dto.getCombatDifficulty())
.enemies(dto.getEnemies())
.relatedPageIds(dto.getRelatedPageIds() != null
? new ArrayList<>(dto.getRelatedPageIds())
: new ArrayList<>())
.build();
}
}

View File

@@ -0,0 +1,47 @@
package com.loremind.infrastructure.web.mapper;
import com.loremind.domain.lorecontext.Template;
import com.loremind.infrastructure.web.dto.lorecontext.TemplateDTO;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/**
* Mapper pour convertir entre Template (entité de domaine) et TemplateDTO.
*/
@Component
public class TemplateMapper {
public TemplateDTO toDTO(Template template) {
if (template == null) {
return null;
}
TemplateDTO dto = new TemplateDTO();
dto.setId(template.getId());
dto.setLoreId(template.getLoreId());
dto.setName(template.getName());
dto.setDescription(template.getDescription());
dto.setDefaultNodeId(template.getDefaultNodeId());
dto.setFields(template.getFields() != null
? new ArrayList<>(template.getFields())
: new ArrayList<>());
dto.setFieldCount(template.fieldCount());
return dto;
}
public Template toDomain(TemplateDTO dto) {
if (dto == null) {
return null;
}
return Template.builder()
.id(dto.getId())
.loreId(dto.getLoreId())
.name(dto.getName())
.description(dto.getDescription())
.defaultNodeId(dto.getDefaultNodeId())
.fields(dto.getFields() != null
? new ArrayList<>(dto.getFields())
: new ArrayList<>())
.build();
}
}

View File

@@ -0,0 +1,20 @@
# Configuration du serveur
server.port=8080
# Configuration de la base de données PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/loremind
spring.datasource.username=ietm64
spring.datasource.password=REDACTED
spring.datasource.driver-class-name=org.postgresql.Driver
# Configuration JPA / Hibernate
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Configuration CORS pour autoriser le Frontend Angular
spring.web.cors.allowed-origins=http://localhost:4200
spring.web.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
spring.web.cors.allowed-headers=*
spring.web.cors.allow-credentials=true

View File

@@ -0,0 +1,103 @@
package com.loremind.infrastructure.persistence.postgres;
import com.loremind.domain.lorecontext.Lore;
import com.loremind.domain.lorecontext.ports.LoreRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
/**
* Test pour vérifier que PostgresLoreRepository fonctionne correctement.
* Utilise PostgreSQL (loremind_test) pour les tests d'intégration.
*/
@SpringBootTest
public class PostgresLoreRepositoryTest {
@Autowired
private LoreRepository loreRepository;
@Test
public void testSaveAndFindLore() {
// Créer un Lore
Lore lore = Lore.builder()
.name("Lore Test")
.description("Description test")
.nodeCount(0)
.pageCount(0)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
// Sauvegarder
Lore savedLore = loreRepository.save(lore);
assertNotNull(savedLore.getId());
// Récupérer
Optional<Lore> foundLore = loreRepository.findById(savedLore.getId());
assertTrue(foundLore.isPresent());
assertEquals("Lore Test", foundLore.get().getName());
// Nettoyer
loreRepository.deleteById(savedLore.getId());
}
@Test
public void testFindAllLores() {
// Créer deux Lores
Lore lore1 = Lore.builder()
.name("Lore 1")
.description("Description 1")
.nodeCount(0)
.pageCount(0)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
Lore lore2 = Lore.builder()
.name("Lore 2")
.description("Description 2")
.nodeCount(0)
.pageCount(0)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
Lore saved1 = loreRepository.save(lore1);
Lore saved2 = loreRepository.save(lore2);
// Récupérer tous
List<Lore> allLores = loreRepository.findAll();
assertTrue(allLores.size() >= 2);
// Nettoyer
loreRepository.deleteById(saved1.getId());
loreRepository.deleteById(saved2.getId());
}
@Test
public void testDeleteLore() {
// Créer un Lore
Lore lore = Lore.builder()
.name("Lore to delete")
.description("Description")
.nodeCount(0)
.pageCount(0)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
Lore savedLore = loreRepository.save(lore);
// Supprimer
loreRepository.deleteById(savedLore.getId());
// Vérifier qu'il n'existe plus
assertFalse(loreRepository.existsById(savedLore.getId()));
}
}

View File

@@ -0,0 +1,10 @@
# Configuration de test avec PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/loremind_test
spring.datasource.username=ietm64
spring.datasource.password=REDACTED
spring.datasource.driver-class-name=org.postgresql.Driver
# Configuration JPA pour les tests
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true