Skip to main content

things3_core/mutations/
mod.rs

1//! Mutation backend abstraction.
2//!
3//! This module defines [`MutationBackend`], a trait that abstracts every Things 3
4//! mutation operation behind a single interface. The MCP server holds an
5//! `Arc<dyn MutationBackend>` and dispatches all writes through it. This unblocks
6//! issue #120's migration from direct SQLite writes (which CulturedCode warns can
7//! corrupt the user's database) to AppleScript-based mutations.
8//!
9//! Two implementations are planned:
10//! - [`SqlxBackend`] — wraps the existing direct-DB writes on [`crate::ThingsDatabase`].
11//!   Today's behavior; useful for offline tests and CI.
12//! - `AppleScriptBackend` — to be added in #124. The default in production after #125.
13//!
14//! ## Why `#[async_trait]` instead of native `async fn` in traits
15//!
16//! The trait must be object-safe so the server can hold `Arc<dyn MutationBackend>`
17//! and choose between backends at runtime. Native async-fn-in-trait (Rust 1.75+)
18//! requires `#[trait_variant]` shims for `dyn` dispatch and produces unnameable
19//! opaque return types — too much friction for marginal benefit. `#[async_trait]`
20//! boxes the future, which is exactly what `dyn` needs.
21
22use async_trait::async_trait;
23
24use crate::error::Result as ThingsResult;
25use crate::models::{
26    BulkCompleteRequest, BulkCreateTasksRequest, BulkDeleteRequest, BulkMoveRequest,
27    BulkOperationResult, BulkUpdateDatesRequest, CreateAreaRequest, CreateProjectRequest,
28    CreateTagRequest, CreateTaskRequest, DeleteChildHandling, ProjectChildHandling,
29    TagAssignmentResult, TagCreationResult, TagMatch, ThingsId, UpdateAreaRequest,
30    UpdateProjectRequest, UpdateTagRequest, UpdateTaskRequest,
31};
32
33mod sqlx;
34pub use sqlx::SqlxBackend;
35
36#[cfg(target_os = "macos")]
37mod applescript;
38#[cfg(target_os = "macos")]
39pub use applescript::AppleScriptBackend;
40
41/// Abstraction over every Things 3 mutation operation exposed as an MCP tool.
42///
43/// All implementations must be `Send + Sync` so the server can share them across
44/// async tasks via `Arc<dyn MutationBackend>`.
45#[async_trait]
46pub trait MutationBackend: Send + Sync {
47    /// Static identifier for the backend implementation. Used by the MCP server
48    /// to expose which backend is in use (`"sqlx"` direct-DB vs. `"applescript"`
49    /// CulturedCode-supported) without an `Any` downcast.
50    fn kind(&self) -> &'static str {
51        "unknown"
52    }
53
54    // ---- Tasks ----
55
56    async fn create_task(&self, request: CreateTaskRequest) -> ThingsResult<ThingsId>;
57    /// Create multiple tasks in one call. Best-effort and non-atomic — per-item
58    /// failures are reported via `BulkOperationResult`.
59    async fn bulk_create_tasks(
60        &self,
61        request: BulkCreateTasksRequest,
62    ) -> ThingsResult<BulkOperationResult>;
63    async fn update_task(&self, request: UpdateTaskRequest) -> ThingsResult<()>;
64    async fn complete_task(&self, id: &ThingsId) -> ThingsResult<()>;
65    async fn uncomplete_task(&self, id: &ThingsId) -> ThingsResult<()>;
66    async fn delete_task(
67        &self,
68        id: &ThingsId,
69        child_handling: DeleteChildHandling,
70    ) -> ThingsResult<()>;
71    async fn bulk_delete(&self, request: BulkDeleteRequest) -> ThingsResult<BulkOperationResult>;
72    async fn bulk_move(&self, request: BulkMoveRequest) -> ThingsResult<BulkOperationResult>;
73    async fn bulk_update_dates(
74        &self,
75        request: BulkUpdateDatesRequest,
76    ) -> ThingsResult<BulkOperationResult>;
77    async fn bulk_complete(
78        &self,
79        request: BulkCompleteRequest,
80    ) -> ThingsResult<BulkOperationResult>;
81
82    // ---- Projects ----
83
84    async fn create_project(&self, request: CreateProjectRequest) -> ThingsResult<ThingsId>;
85    async fn update_project(&self, request: UpdateProjectRequest) -> ThingsResult<()>;
86    async fn complete_project(
87        &self,
88        id: &ThingsId,
89        child_handling: ProjectChildHandling,
90    ) -> ThingsResult<()>;
91    async fn delete_project(
92        &self,
93        id: &ThingsId,
94        child_handling: ProjectChildHandling,
95    ) -> ThingsResult<()>;
96
97    // ---- Areas ----
98
99    async fn create_area(&self, request: CreateAreaRequest) -> ThingsResult<ThingsId>;
100    async fn update_area(&self, request: UpdateAreaRequest) -> ThingsResult<()>;
101    async fn delete_area(&self, id: &ThingsId) -> ThingsResult<()>;
102
103    // ---- Tags ----
104
105    /// Create a tag. When `force` is true, skip duplicate / similarity checks
106    /// (mirrors the legacy `create_tag_force` path); otherwise run the smart
107    /// flow that may return `Existing` or `SimilarFound`.
108    async fn create_tag(
109        &self,
110        request: CreateTagRequest,
111        force: bool,
112    ) -> ThingsResult<TagCreationResult>;
113    async fn update_tag(&self, request: UpdateTagRequest) -> ThingsResult<()>;
114    async fn delete_tag(&self, id: &ThingsId, remove_from_tasks: bool) -> ThingsResult<()>;
115    async fn merge_tags(&self, source_id: &ThingsId, target_id: &ThingsId) -> ThingsResult<()>;
116    async fn add_tag_to_task(
117        &self,
118        task_id: &ThingsId,
119        tag_title: &str,
120    ) -> ThingsResult<TagAssignmentResult>;
121    async fn remove_tag_from_task(&self, task_id: &ThingsId, tag_title: &str) -> ThingsResult<()>;
122    async fn set_task_tags(
123        &self,
124        task_id: &ThingsId,
125        tag_titles: Vec<String>,
126    ) -> ThingsResult<Vec<TagMatch>>;
127}