From 3fe0d3daa564bff5d17d4ae2c090d025bfdaf42a Mon Sep 17 00:00:00 2001 From: Pier-Jean Malandrino Date: Mon, 4 May 2026 13:37:44 +0200 Subject: [PATCH] feat(#251): add Neo4j as a StoreKind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - domain : StoreKind.NEO4J ajouté à l'enum - service : validation config par kind étendue (Neo4j requires non-empty index_name) - frontend : Neo4jConfigForm (index_name + database optionnelle), dispatcher StoreConfigForm, dropdown kind, i18n FR/EN - tests service : 2 cas Neo4j (create OK + missing index_name 422) Auth (URI/user/password) reste pilotée par les variables d'environnement globales (cf. infra/neo4j.py) — la config par store ne porte que les paramètres routables (index, database). --- document-parser/domain/value_objects.py | 6 +- document-parser/services/store_service.py | 10 +- document-parser/tests/test_store_service.py | 21 ++++ .../src/features/store/ui/Neo4jConfigForm.vue | 109 ++++++++++++++++++ .../src/features/store/ui/StoreConfigForm.vue | 3 + frontend/src/features/store/ui/StoreForm.vue | 5 +- frontend/src/shared/i18n.ts | 11 ++ 7 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 frontend/src/features/store/ui/Neo4jConfigForm.vue diff --git a/document-parser/domain/value_objects.py b/document-parser/domain/value_objects.py index cf9b258..af973b5 100644 --- a/document-parser/domain/value_objects.py +++ b/document-parser/domain/value_objects.py @@ -40,11 +40,11 @@ class DocumentLifecycleState(StrEnum): class StoreKind(StrEnum): - """Backing technology of a Store. Today only OpenSearch is implemented; - the enum is here so future backends (Pinecone, Qdrant, pgvector) can be - added without touching the persistence schema.""" + """Backing technology of a Store. New backends plug in here without + touching the persistence schema.""" OPENSEARCH = "opensearch" + NEO4J = "neo4j" class DocumentStoreLinkState(StrEnum): diff --git a/document-parser/services/store_service.py b/document-parser/services/store_service.py index bee40a5..13e2000 100644 --- a/document-parser/services/store_service.py +++ b/document-parser/services/store_service.py @@ -87,13 +87,17 @@ def _validate_slug(slug: str) -> None: def _validate_config_for_kind(kind: StoreKind, config: dict) -> None: - """Per-kind config schema. Today only OpenSearch is wired; new kinds plug - in here without touching the rest of the pipeline.""" + """Per-kind config schema. New kinds plug in here without touching the + rest of the pipeline. Authentication for remote stores comes from the + deployment env (see `infra/settings.py`).""" if kind is StoreKind.OPENSEARCH: index_name = config.get("index_name") or config.get("indexName") if not isinstance(index_name, str) or not index_name.strip(): raise StoreValidationError("OpenSearch config requires a non-empty 'index_name'") - # Future: add LlamaIndex / LangChain / pgvector branches here. + elif kind is StoreKind.NEO4J: + index_name = config.get("index_name") or config.get("indexName") + if not isinstance(index_name, str) or not index_name.strip(): + raise StoreValidationError("Neo4j config requires a non-empty 'index_name'") class StoreService: diff --git a/document-parser/tests/test_store_service.py b/document-parser/tests/test_store_service.py index 31cab80..19b3996 100644 --- a/document-parser/tests/test_store_service.py +++ b/document-parser/tests/test_store_service.py @@ -110,6 +110,27 @@ class TestCreate: config={}, ) + async def test_create_neo4j_ok(self, service): + store = await service.create_store( + name="kg", + slug="kg", + kind=StoreKind.NEO4J, + embedder="bge-m3", + config={"index_name": "chunks-vec", "database": "neo4j"}, + ) + assert store.kind is StoreKind.NEO4J + assert store.config["index_name"] == "chunks-vec" + + async def test_create_neo4j_rejects_missing_index_name(self, service): + with pytest.raises(StoreValidationError): + await service.create_store( + name="kg", + slug="kg", + kind=StoreKind.NEO4J, + embedder="bge-m3", + config={}, + ) + async def test_create_default_clears_others(self, service): # The seeded 'default' store starts as is_default=True. new_default = await service.create_store( diff --git a/frontend/src/features/store/ui/Neo4jConfigForm.vue b/frontend/src/features/store/ui/Neo4jConfigForm.vue new file mode 100644 index 0000000..c710250 --- /dev/null +++ b/frontend/src/features/store/ui/Neo4jConfigForm.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/frontend/src/features/store/ui/StoreConfigForm.vue b/frontend/src/features/store/ui/StoreConfigForm.vue index 2db8a72..ed742de 100644 --- a/frontend/src/features/store/ui/StoreConfigForm.vue +++ b/frontend/src/features/store/ui/StoreConfigForm.vue @@ -10,6 +10,7 @@