mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
187 lines
5.1 KiB
Python
187 lines
5.1 KiB
Python
"""Wire schemas spoken between the SurfSense Obsidian plugin and the backend.
|
|
|
|
All schemas inherit ``extra='ignore'`` from :class:`_PluginBase` so additive
|
|
field changes never break either side; hard breaks live behind a new URL
|
|
prefix (``/api/v2/...``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
_PLUGIN_MODEL_CONFIG = ConfigDict(extra="ignore")
|
|
|
|
|
|
class _PluginBase(BaseModel):
|
|
"""Base schema carrying the shared forward-compatibility config."""
|
|
|
|
model_config = _PLUGIN_MODEL_CONFIG
|
|
|
|
|
|
class HeadingRef(_PluginBase):
|
|
"""One markdown heading extracted from Obsidian metadata cache."""
|
|
|
|
heading: str
|
|
level: int = Field(ge=1, le=6)
|
|
|
|
|
|
class NotePayload(_PluginBase):
|
|
"""One Obsidian note as pushed by the plugin (the source of truth)."""
|
|
|
|
vault_id: str = Field(
|
|
..., description="Stable plugin-generated UUID for this vault"
|
|
)
|
|
path: str = Field(..., description="Vault-relative path, e.g. 'notes/foo.md'")
|
|
name: str = Field(..., description="File stem (no extension)")
|
|
extension: str = Field(
|
|
default="md", description="File extension without leading dot"
|
|
)
|
|
content: str = Field(default="", description="Raw markdown body (post-frontmatter)")
|
|
|
|
frontmatter: dict[str, Any] = Field(default_factory=dict)
|
|
tags: list[str] = Field(default_factory=list)
|
|
headings: list[HeadingRef] = Field(default_factory=list)
|
|
resolved_links: list[str] = Field(default_factory=list)
|
|
unresolved_links: list[str] = Field(default_factory=list)
|
|
embeds: list[str] = Field(default_factory=list)
|
|
aliases: list[str] = Field(default_factory=list)
|
|
|
|
content_hash: str = Field(
|
|
..., description="Plugin-computed SHA-256 of the raw content"
|
|
)
|
|
size: int | None = Field(
|
|
default=None,
|
|
ge=0,
|
|
description="Byte size of the local file (mtime+size short-circuit signal). Optional for forward compatibility.",
|
|
)
|
|
mtime: datetime
|
|
ctime: datetime
|
|
|
|
|
|
class SyncBatchRequest(_PluginBase):
|
|
"""Batch upsert; plugin sends 10-20 notes per request."""
|
|
|
|
vault_id: str
|
|
notes: list[NotePayload] = Field(default_factory=list, max_length=100)
|
|
|
|
|
|
class RenameItem(_PluginBase):
|
|
old_path: str
|
|
new_path: str
|
|
|
|
|
|
class RenameBatchRequest(_PluginBase):
|
|
vault_id: str
|
|
renames: list[RenameItem] = Field(default_factory=list, max_length=200)
|
|
|
|
|
|
class DeleteBatchRequest(_PluginBase):
|
|
vault_id: str
|
|
paths: list[str] = Field(default_factory=list, max_length=500)
|
|
|
|
|
|
class ManifestEntry(_PluginBase):
|
|
hash: str
|
|
mtime: datetime
|
|
size: int | None = Field(
|
|
default=None,
|
|
description="Byte size last seen by the server. Enables mtime+size short-circuit; absent when not yet recorded.",
|
|
)
|
|
|
|
|
|
class ManifestResponse(_PluginBase):
|
|
"""Path-keyed manifest of every non-deleted note for a vault."""
|
|
|
|
vault_id: str
|
|
items: dict[str, ManifestEntry] = Field(default_factory=dict)
|
|
|
|
|
|
class ConnectRequest(_PluginBase):
|
|
"""Vault registration / heartbeat. Replayed on every plugin onload."""
|
|
|
|
vault_id: str
|
|
vault_name: str
|
|
search_space_id: int
|
|
vault_fingerprint: str = Field(
|
|
...,
|
|
description=(
|
|
"Deterministic SHA-256 over the sorted markdown paths in the vault "
|
|
"(plus vault_name). Same vault content on any device produces the "
|
|
"same value; the server uses it to dedup connectors across devices."
|
|
),
|
|
)
|
|
|
|
|
|
class ConnectResponse(_PluginBase):
|
|
"""Carries the same handshake fields as ``HealthResponse`` so the plugin
|
|
learns the contract without a separate ``GET /health`` round-trip."""
|
|
|
|
connector_id: int
|
|
vault_id: str
|
|
search_space_id: int
|
|
capabilities: list[str]
|
|
|
|
|
|
class HealthResponse(_PluginBase):
|
|
"""API contract handshake. ``capabilities`` is additive-only string list."""
|
|
|
|
capabilities: list[str]
|
|
server_time_utc: datetime
|
|
|
|
|
|
# Per-item batch ack schemas — wire shape is load-bearing for the plugin
|
|
# queue (see api-client.ts / sync-engine.ts:processBatch).
|
|
|
|
|
|
class SyncAckItem(_PluginBase):
|
|
path: str
|
|
status: Literal["ok", "error"]
|
|
document_id: int | None = None
|
|
error: str | None = None
|
|
|
|
|
|
class SyncAck(_PluginBase):
|
|
vault_id: str
|
|
indexed: int
|
|
failed: int
|
|
items: list[SyncAckItem] = Field(default_factory=list)
|
|
|
|
|
|
class RenameAckItem(_PluginBase):
|
|
old_path: str
|
|
new_path: str
|
|
# ``missing`` is treated as success client-side (end state reached).
|
|
status: Literal["ok", "error", "missing"]
|
|
document_id: int | None = None
|
|
error: str | None = None
|
|
|
|
|
|
class RenameAck(_PluginBase):
|
|
vault_id: str
|
|
renamed: int
|
|
missing: int
|
|
items: list[RenameAckItem] = Field(default_factory=list)
|
|
|
|
|
|
class DeleteAckItem(_PluginBase):
|
|
path: str
|
|
status: Literal["ok", "error", "missing"]
|
|
error: str | None = None
|
|
|
|
|
|
class DeleteAck(_PluginBase):
|
|
vault_id: str
|
|
deleted: int
|
|
missing: int
|
|
items: list[DeleteAckItem] = Field(default_factory=list)
|
|
|
|
|
|
class StatsResponse(_PluginBase):
|
|
"""Backs the Obsidian connector tile in the web UI."""
|
|
|
|
vault_id: str
|
|
files_synced: int
|
|
last_sync_at: datetime | None = None
|