File size: 3,289 Bytes
02af15b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
"""Note-related Pydantic models."""

from __future__ import annotations

from datetime import datetime
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field, field_validator


class NoteMetadata(BaseModel):
    """Frontmatter metadata (allows arbitrary keys)."""

    model_config = ConfigDict(extra="allow")

    title: Optional[str] = None
    tags: Optional[list[str]] = None
    project: Optional[str] = None
    created: Optional[datetime] = None
    updated: Optional[datetime] = None


class Note(BaseModel):
    """Complete note with content and metadata."""

    model_config = ConfigDict(
        json_schema_extra={
            "example": {
                "user_id": "alice",
                "note_path": "api/design.md",
                "version": 5,
                "title": "API Design",
                "metadata": {
                    "tags": ["backend", "api"],
                    "project": "auth-service",
                },
                "body": "# API Design\\n\\nThis document describes...",
                "created": "2025-01-10T09:00:00Z",
                "updated": "2025-01-15T14:30:00Z",
                "size_bytes": 4096,
            }
        }
    )

    user_id: str = Field(..., description="Owner user ID")
    note_path: str = Field(
        ...,
        min_length=1,
        max_length=256,
        description="Relative path to vault root (includes .md)",
    )
    version: int = Field(..., ge=1, description="Optimistic concurrency version")
    title: str = Field(..., min_length=1, description="Display title")
    metadata: NoteMetadata = Field(default_factory=NoteMetadata, description="Frontmatter")
    body: str = Field(..., description="Markdown content")
    created: datetime = Field(..., description="Creation timestamp")
    updated: datetime = Field(..., description="Last update timestamp")
    size_bytes: int = Field(..., ge=0, le=1_048_576, description="File size in bytes")

    @field_validator("note_path")
    @classmethod
    def validate_path(cls, value: str) -> str:
        if not value.endswith(".md"):
            raise ValueError("Note path must end with .md")
        if ".." in value:
            raise ValueError("Note path must not contain '..'")
        if "\\" in value:
            raise ValueError("Note path must use Unix-style separators (/)")
        if value.startswith("/"):
            raise ValueError("Note path must be relative (no leading /)")
        return value


class NoteCreate(BaseModel):
    """Request payload to create a note."""

    note_path: str = Field(..., min_length=1, max_length=256)
    title: Optional[str] = None
    metadata: Optional[NoteMetadata] = None
    body: str = Field(..., max_length=1_048_576)


class NoteUpdate(BaseModel):
    """Request payload to update a note."""

    title: Optional[str] = None
    metadata: Optional[NoteMetadata] = None
    body: str = Field(..., max_length=1_048_576)
    if_version: Optional[int] = Field(
        None, ge=1, description="Expected version for concurrency check"
    )


class NoteSummary(BaseModel):
    """Lightweight representation used for listings."""

    note_path: str
    title: str
    updated: datetime


__all__ = ["NoteMetadata", "Note", "NoteCreate", "NoteUpdate", "NoteSummary"]