mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-31 19:45:15 +02:00
feat(event_bus): add document.entered_folder event type and payload schema
This commit is contained in:
parent
2a511b8559
commit
731d5231ff
3 changed files with 147 additions and 0 deletions
5
surfsense_backend/app/event_bus/events/__init__.py
Normal file
5
surfsense_backend/app/event_bus/events/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""Domain event type definitions — each in its own module, self-registering at import."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import document_entered_folder # noqa: F401
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
"""``document.entered_folder``: a document became a member of a folder.
|
||||||
|
|
||||||
|
Fires once per arrival, however the document got there (upload, AI sort, move).
|
||||||
|
The payload carries the fields a user can filter a trigger on.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, computed_field
|
||||||
|
|
||||||
|
from app.event_bus.catalog import EventType, catalog
|
||||||
|
|
||||||
|
EVENT_TYPE = "document.entered_folder"
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentEnteredFolderPayload(BaseModel):
|
||||||
|
"""Snapshot of the document at the moment it entered ``folder_id``.
|
||||||
|
|
||||||
|
``previous_folder_id`` is the folder it left, or ``None`` for a first
|
||||||
|
placement. ``is_move`` derives from it and is emitted for filtering.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
document_id: int
|
||||||
|
folder_id: int
|
||||||
|
previous_folder_id: int | None = None
|
||||||
|
document_type: str
|
||||||
|
title: str
|
||||||
|
connector_id: int | None = None
|
||||||
|
created_by_id: str | None = None
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def is_move(self) -> bool:
|
||||||
|
return self.previous_folder_id is not None
|
||||||
|
|
||||||
|
|
||||||
|
catalog.register(
|
||||||
|
EventType(
|
||||||
|
type=EVENT_TYPE,
|
||||||
|
description="A document became a member of a folder.",
|
||||||
|
payload_model=DocumentEnteredFolderPayload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def payload_if_entered_folder(
|
||||||
|
*,
|
||||||
|
document_id: int,
|
||||||
|
search_space_id: int,
|
||||||
|
new_folder_id: int | None,
|
||||||
|
previous_folder_id: int | None,
|
||||||
|
folder_id_changed: bool,
|
||||||
|
status_state: str,
|
||||||
|
document_type: str,
|
||||||
|
title: str,
|
||||||
|
connector_id: int | None,
|
||||||
|
created_by_id: str | None,
|
||||||
|
) -> dict | None:
|
||||||
|
"""Return a publish payload if this commit represents a folder arrival, else None.
|
||||||
|
|
||||||
|
``folder_id_changed`` comes from SQLAlchemy attribute history — it is True
|
||||||
|
only when ``folder_id`` actually changed in this transaction, preventing
|
||||||
|
spurious events on unrelated saves.
|
||||||
|
"""
|
||||||
|
if not folder_id_changed:
|
||||||
|
return None
|
||||||
|
if new_folder_id is None:
|
||||||
|
return None
|
||||||
|
if status_state != "ready":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"event_type": EVENT_TYPE,
|
||||||
|
"search_space_id": search_space_id,
|
||||||
|
"payload": {
|
||||||
|
"document_id": document_id,
|
||||||
|
"folder_id": new_folder_id,
|
||||||
|
"previous_folder_id": previous_folder_id,
|
||||||
|
"document_type": document_type,
|
||||||
|
"title": title,
|
||||||
|
"connector_id": connector_id,
|
||||||
|
"created_by_id": created_by_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
"""``document.entered_folder`` payload contract + catalog registration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.event_bus.catalog import catalog
|
||||||
|
from app.event_bus.events.document_entered_folder import (
|
||||||
|
EVENT_TYPE,
|
||||||
|
DocumentEnteredFolderPayload,
|
||||||
|
)
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.unit
|
||||||
|
|
||||||
|
|
||||||
|
def _payload(**overrides: object) -> DocumentEnteredFolderPayload:
|
||||||
|
base: dict[str, object] = {
|
||||||
|
"document_id": 42,
|
||||||
|
"folder_id": 7,
|
||||||
|
"document_type": "FILE",
|
||||||
|
"title": "Q3 report.pdf",
|
||||||
|
}
|
||||||
|
base.update(overrides)
|
||||||
|
return DocumentEnteredFolderPayload(**base)
|
||||||
|
|
||||||
|
|
||||||
|
def test_payload_carries_the_filterable_fields() -> None:
|
||||||
|
payload = _payload(connector_id=12, created_by_id="abc")
|
||||||
|
|
||||||
|
assert payload.document_id == 42
|
||||||
|
assert payload.folder_id == 7
|
||||||
|
assert payload.document_type == "FILE"
|
||||||
|
assert payload.connector_id == 12
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_placement_is_not_a_move() -> None:
|
||||||
|
"""No previous folder (created or AI-sorted into place) → not a move."""
|
||||||
|
assert _payload(previous_folder_id=None).is_move is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_between_folders_is_a_move() -> None:
|
||||||
|
assert _payload(previous_folder_id=3).is_move is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_move_is_serialized_for_filtering() -> None:
|
||||||
|
"""Filters match against the dumped payload, so ``is_move`` must appear there."""
|
||||||
|
dumped = _payload(previous_folder_id=3).model_dump()
|
||||||
|
|
||||||
|
assert dumped["is_move"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_type_is_registered_in_the_catalog() -> None:
|
||||||
|
registered = catalog.get(EVENT_TYPE)
|
||||||
|
|
||||||
|
assert registered is not None
|
||||||
|
assert registered.payload_model is DocumentEnteredFolderPayload
|
||||||
Loading…
Add table
Add a link
Reference in a new issue