mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
docs(architecture): enforce host persistence boundary
This commit is contained in:
@@ -110,7 +110,9 @@ eventmanager.send_event(EventType.TransferComplete, data_dict)
|
||||
|
||||
## 4. Repository (Oper) Pattern
|
||||
|
||||
**When to use:** All database reads and writes. Never issue SQLAlchemy queries directly from chain, module, or endpoint code.
|
||||
**When to use:** Implementing table-oriented persistence behind an Application-owned
|
||||
Protocol. Never issue SQLAlchemy queries or construct Oper objects directly from
|
||||
chain, module, endpoint, Agent, scheduler, or workflow code.
|
||||
|
||||
**Convention:** Each SQLAlchemy model in `app/db/models/` has a corresponding `<Model>Oper` class in `app/db/oper/<model>.py` — the two packages mirror each other file for file, so the module name carries the entity and the package carries the role.
|
||||
|
||||
@@ -120,16 +122,36 @@ app/db/models/systemconfig.py → app/db/oper/systemconfig.py (SystemConfi
|
||||
app/db/models/transferhistory.py → app/db/oper/transferhistory.py (TransferHistoryOper)
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
**Host usage:**
|
||||
|
||||
Host entrypoints depend on an Application service or Protocol. The concrete
|
||||
`app/db/adapters/` implementation creates one operation-scoped Session, adapts the
|
||||
Protocol with Oper objects, and gives commit/rollback ownership to the Application
|
||||
command. Oper methods only query, stage, or flush in that caller-owned Session.
|
||||
|
||||
```python
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.application.subscription.write import SubscribeWriter
|
||||
|
||||
oper = SubscribeOper()
|
||||
subscribe = oper.get(sid=1)
|
||||
oper.add(Subscribe(name="Example", type="电影"))
|
||||
def create_subscription(writer: SubscribeWriter, identity: dict, payload: dict):
|
||||
return writer.add(identity=identity, payload=payload)
|
||||
```
|
||||
|
||||
The explicit Session/Oper composition belongs in `app/db/adapters/`, not at the
|
||||
host call site:
|
||||
|
||||
```python
|
||||
with SessionFactory() as session:
|
||||
command = CreateSubscriptionCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
)
|
||||
result = command.execute(identity=identity, payload=payload)
|
||||
```
|
||||
|
||||
The no-Session `SubscribeOper()` facade is legacy plugin ABI only. It may remain
|
||||
available through the curated SDK/Compat boundary, but new host code and examples
|
||||
must not copy that form.
|
||||
|
||||
---
|
||||
|
||||
## 5. Config Reload Pattern
|
||||
@@ -180,17 +202,21 @@ Do not introduce new singletons unless the class genuinely manages global shared
|
||||
|
||||
**Enum:** `SystemConfigKey` in `app/schemas/types.py`
|
||||
|
||||
**Oper class:** `SystemConfigOper` in `app/db/oper/systemconfig.py`
|
||||
**Host service:** `SystemConfigService` and `get_configured_system_config()` in
|
||||
`app/application/configuration.py`.
|
||||
|
||||
```python
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
|
||||
oper = SystemConfigOper()
|
||||
value = oper.get(SystemConfigKey.RssUrls)
|
||||
oper.set(SystemConfigKey.RssUrls, ["https://..."])
|
||||
configuration = get_configured_system_config()
|
||||
value = configuration.get(SystemConfigKey.RssUrls)
|
||||
configuration.set(SystemConfigKey.RssUrls, ["https://..."])
|
||||
```
|
||||
|
||||
`SystemConfigOper` is the DB/composition implementation and legacy plugin-facing
|
||||
facade; canonical host consumers do not construct it.
|
||||
|
||||
**Rule:** Never use raw string literals as SystemConfig keys. Always add a new entry to the `SystemConfigKey` enum first.
|
||||
|
||||
---
|
||||
@@ -199,9 +225,12 @@ oper.set(SystemConfigKey.RssUrls, ["https://..."])
|
||||
|
||||
**When to use:** Per-user settings that must survive across sessions but differ by user.
|
||||
|
||||
**Oper class:** `UserConfigOper` in `app/db/oper/userconfig.py`
|
||||
**Host service:** `UserConfigurationService` and
|
||||
`get_configured_user_configuration()` in
|
||||
`app/application/security/userconfig.py`.
|
||||
|
||||
Usage mirrors `SystemConfigOper` but scoped to a `user_id`.
|
||||
The concrete repository uses `UserConfigOper` behind the service boundary. Host
|
||||
callers do not construct it.
|
||||
|
||||
---
|
||||
|
||||
@@ -212,8 +241,8 @@ Usage mirrors `SystemConfigOper` but scoped to a `user_id`.
|
||||
| `module -> chain` coupling | Move orchestration into `chain` and shared logic into its owning canonical package |
|
||||
| `module -> module` direct calls | Use `chain` to orchestrate cross-module workflows |
|
||||
| Lower-level module importing a chain or manager | Register a callback/resolver from `app/startup/` or move orchestration to `chain` |
|
||||
| Raw SQLAlchemy queries in endpoints or chains | Use the corresponding Oper class in `app/db/oper/` |
|
||||
| Raw SQLAlchemy queries or Oper construction in host entrypoints | Define/use an Application Protocol or command; implement it in `app/db/adapters/` with an operation-scoped Session/UoW |
|
||||
| Raw string keys for SystemConfig | Define and use a `SystemConfigKey` enum entry |
|
||||
| HTTP requests via `requests` or `httpx` directly | Host code uses `RequestUtils` from `app/adapters/network/http.py`; plugins use `app.sdk.network` |
|
||||
|
||||
*Last Updated: 2026-08-14*
|
||||
*Last Updated: 2026-08-27*
|
||||
|
||||
@@ -116,9 +116,9 @@ All new code must follow these conventions. Consistent naming is how the codebas
|
||||
| `def GetSubscribe():` | `def get_subscribe():` |
|
||||
| `TORRENT_info = ...` | `torrent_info = ...` |
|
||||
| `def handleConfigChanged():` | `def on_config_changed():` or `def handle_config_changed():` |
|
||||
| `SystemConfigOper().get("RssUrls")` | `SystemConfigOper().get(SystemConfigKey.RssUrls)` |
|
||||
| `configuration.get("RssUrls")` | `configuration.get(SystemConfigKey.RssUrls)` |
|
||||
| `class subscribe_oper:` | `class SubscribeOper:` |
|
||||
| `MessageChannel.Telegram`(新代码) | `NotificationChannel.Telegram` |
|
||||
| `Notification(title=...)`(新代码) | `Message(title=...)` |
|
||||
|
||||
*Last Updated: 2026-08-16*
|
||||
*Last Updated: 2026-08-27*
|
||||
|
||||
@@ -97,7 +97,7 @@ if not self._initialized:
|
||||
|
||||
```python
|
||||
# 获取订阅列表 ← 这只是在重述代码,不需要
|
||||
subscribes = SubscribeOper().list()
|
||||
subscribes = repository.list()
|
||||
|
||||
# 如果 result 为 None 则返回 ← 无意义
|
||||
if result is None:
|
||||
@@ -139,4 +139,4 @@ When modifying code, update or remove any comment that no longer accurately desc
|
||||
| Commented-out dead code | Delete it; git history preserves it |
|
||||
| New contract documentation in English inside an otherwise Chinese file | Breaks the repository's default documentation language and local consistency |
|
||||
|
||||
*Last Updated: 2026-08-13*
|
||||
*Last Updated: 2026-08-27*
|
||||
|
||||
@@ -199,19 +199,21 @@ oper.delete(sid=1) # Delete by key
|
||||
|
||||
**Enum:** `SystemConfigKey` in `app/schemas/types.py`
|
||||
|
||||
**Oper:** `SystemConfigOper` in `app/db/oper/systemconfig.py`
|
||||
**Host service:** `SystemConfigService` and `get_configured_system_config()` in
|
||||
`app/application/configuration.py`. `SystemConfigOper` is used behind the
|
||||
composition/persistence boundary and remains available for legacy plugin ABI.
|
||||
|
||||
```python
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
|
||||
oper = SystemConfigOper()
|
||||
configuration = get_configured_system_config()
|
||||
|
||||
# Read
|
||||
rss_urls = oper.get(SystemConfigKey.RssUrls)
|
||||
rss_urls = configuration.get(SystemConfigKey.RssUrls)
|
||||
|
||||
# Write
|
||||
oper.set(SystemConfigKey.RssUrls, ["https://example.com/rss"])
|
||||
configuration.set(SystemConfigKey.RssUrls, ["https://example.com/rss"])
|
||||
```
|
||||
|
||||
**Rule:** Never use raw string literals as `SystemConfig` keys. Always define a new `SystemConfigKey` enum entry first. Raw string key lookups are not searchable and cannot be refactored safely.
|
||||
@@ -220,16 +222,21 @@ oper.set(SystemConfigKey.RssUrls, ["https://example.com/rss"])
|
||||
|
||||
## UserConfig — Per-User Configuration
|
||||
|
||||
**Purpose:** Settings that differ per user account. Uses `UserConfigOper`.
|
||||
**Purpose:** Settings that differ per user account. Host callers use the configured
|
||||
`UserConfigurationService`; its concrete repository adapts `UserConfigOper` behind
|
||||
the persistence boundary.
|
||||
|
||||
```python
|
||||
from app.db.oper.userconfig import UserConfigOper
|
||||
from app.application.security.userconfig import get_configured_user_configuration
|
||||
|
||||
oper = UserConfigOper()
|
||||
value = oper.get(user_id=1, key="notification_enabled")
|
||||
oper.set(user_id=1, key="notification_enabled", value=True)
|
||||
configuration = get_configured_user_configuration()
|
||||
value = configuration.get(username="alice", key="notification_enabled")
|
||||
configuration.set(username="alice", key="notification_enabled", value=True)
|
||||
```
|
||||
|
||||
The no-Session `UserConfigOper()` form is legacy plugin ABI only and must not be
|
||||
copied into host code.
|
||||
|
||||
---
|
||||
|
||||
## Settings / Environment Configuration
|
||||
@@ -290,4 +297,4 @@ When `REDIS_HOST` is configured, `app/modules/redis/` provides a distributed cac
|
||||
- `settings.API_TOKEN` and other secret fields must not be included in log output or API responses.
|
||||
- The `config list --show-secrets` flag exists specifically to gate secret visibility in the CLI.
|
||||
|
||||
*Last Updated: 2026-08-24*
|
||||
*Last Updated: 2026-08-27*
|
||||
|
||||
Reference in New Issue
Block a user