بکاند: بدنه مقاله به سند ساختاریافته TipTap (JSON) + مهاجرت داده
- body مقاله از لیست پاراگراف به سند ProseMirror تغییر کرد:
{"type": "doc", "content": [...]} — در In/Out/Patch schemas
- اعتبارسنجی ساختاری (type=doc + content لیست) با field_validator —
امنیت رندر بر عهده رندر استاتیک TipTap در فرانتاند است
- app/migrations.py: مهاجرتهای DDL و داده در یکجا؛ migrate_article_bodies
ردیفهای قدیمی (لیست پاراگراف) را idempotent به سند تبدیل میکند —
در startup و seed اجرا میشود، دیتابیس موجود بدون از دست رفتن داده ارتقا مییابد
- seed بدنههای لیستی را قبل از درج به سند تبدیل میکند
- README بکاند: فرمت بدنه و رفتار مهاجرت مستند شد
This commit is contained in:
+26
-4
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, create_model
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, create_model, field_validator
|
||||
|
||||
# ---------- پایه ----------
|
||||
|
||||
@@ -58,7 +58,7 @@ class ArticleOut(ORMModel):
|
||||
image: str
|
||||
read: str
|
||||
excerpt: str
|
||||
body: list[str]
|
||||
body: dict
|
||||
|
||||
|
||||
class TestimonialOut(ORMModel):
|
||||
@@ -318,6 +318,18 @@ class ProjectIn(BaseModel):
|
||||
body: list[str] = []
|
||||
|
||||
|
||||
EMPTY_DOC = {"type": "doc", "content": [{"type": "paragraph"}]}
|
||||
|
||||
|
||||
def validate_doc_body(value: object) -> dict:
|
||||
"""بدنه مقاله باید یک سند TipTap (ProseMirror) معتبر از نظر ساختار باشد."""
|
||||
if not value:
|
||||
return dict(EMPTY_DOC)
|
||||
if not isinstance(value, dict) or value.get("type") != "doc" or not isinstance(value.get("content"), list):
|
||||
raise ValueError("body must be a TipTap doc: {type: 'doc', content: [...]}")
|
||||
return value
|
||||
|
||||
|
||||
class ArticleIn(BaseModel):
|
||||
slug: str = Field(min_length=1, max_length=128)
|
||||
title: str = Field(min_length=1, max_length=256)
|
||||
@@ -326,7 +338,8 @@ class ArticleIn(BaseModel):
|
||||
image: str = ""
|
||||
read: str = ""
|
||||
excerpt: str = ""
|
||||
body: list[str] = []
|
||||
body: dict = Field(default_factory=lambda: dict(EMPTY_DOC))
|
||||
_validate_body = field_validator("body")(validate_doc_body)
|
||||
|
||||
|
||||
class TestimonialIn(BaseModel):
|
||||
@@ -507,7 +520,16 @@ def _make_patch(name: str, model_cls: type[BaseModel]) -> type[BaseModel]:
|
||||
BrandPatch = _make_patch("BrandPatch", BrandIn)
|
||||
ServicePatch = _make_patch("ServicePatch", ServiceIn)
|
||||
ProjectPatch = _make_patch("ProjectPatch", ProjectIn)
|
||||
ArticlePatch = _make_patch("ArticlePatch", ArticleIn)
|
||||
class ArticlePatch(BaseModel):
|
||||
slug: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
date: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
image: Optional[str] = None
|
||||
read: Optional[str] = None
|
||||
excerpt: Optional[str] = None
|
||||
body: Optional[dict] = None
|
||||
_validate_body = field_validator("body")(validate_doc_body)
|
||||
TestimonialPatch = _make_patch("TestimonialPatch", TestimonialIn)
|
||||
FaqPatch = _make_patch("FaqPatch", FaqIn)
|
||||
TeamMemberPatch = _make_patch("TeamMemberPatch", TeamMemberIn)
|
||||
|
||||
Reference in New Issue
Block a user